diff --git a/.gitignore b/.gitignore index fc875a12..acb57448 100644 --- a/.gitignore +++ b/.gitignore @@ -30,9 +30,9 @@ cookbook/appworld/exp_result/* file_vector_store/* cookbook/appworld/file_vector_store/* experiencemaker/tool/web_search_cach/* -experiencemaker/cookbook/bfcl/exp_result/* -experiencemaker/cookbook/bfcl/no_exp_result/* -experiencemaker/cookbook/bfcl/data/* +experiencemaker/cookbook/bfcl/exp_result +experiencemaker/cookbook/bfcl/no_exp_result +experiencemaker/cookbook/bfcl/data experiencemaker/cookbook/bfcl/gorilla experiencemaker/*.sh experiencemaker/file_vector_store \ No newline at end of file diff --git a/experiencemaker/ExperienceMaker.egg-info/PKG-INFO b/experiencemaker/ExperienceMaker.egg-info/PKG-INFO new file mode 100644 index 00000000..b9558f77 --- /dev/null +++ b/experiencemaker/ExperienceMaker.egg-info/PKG-INFO @@ -0,0 +1,889 @@ +Metadata-Version: 2.4 +Name: ExperienceMaker +Version: 0.1.1 +Summary: make experience from trajectory +Author-email: experiencemaker team +License: Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Alibaba Group + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.12 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: dashscope>=1.19.1 +Requires-Dist: elasticsearch>=8.14.0 +Requires-Dist: fastapi>=0.115.13 +Requires-Dist: fastmcp>=2.10.6 +Requires-Dist: loguru>=0.7.3 +Requires-Dist: mcp>=1.9.4 +Requires-Dist: numpy>=2.3.0 +Requires-Dist: openai>=1.88.0 +Requires-Dist: pydantic>=2.11.7 +Requires-Dist: PyYAML>=6.0.2 +Requires-Dist: Requests>=2.32.4 +Requires-Dist: uvicorn>=0.34.3 +Requires-Dist: setuptools>=75.0 +Dynamic: license-file + +# ExperienceMaker + +

+ ExperienceMaker Logo +

+ +

+ Python Version + PyPI Version + License + GitHub Stars +

+ +

+ A comprehensive framework to make & reuse & share experience for AI agent
+ Empowering agents to learn from the past and excel in the future +

+ +--- + +## πŸ“° What's New +- **[2025-08]** πŸš€ MCP is now available! β†’ [Quick Start Guide](./doc/mcp_quick_start.md) +- **[2025-07]** πŸŽ‰ ExperienceMaker v0.1.1 is now available on [PyPI](https://pypi.org/project/experiencemaker/)! +- **[2025-07]** πŸ“š Complete documentation and quick start guides released +- **[2025-06]** πŸš€ Multi-backend vector store support (Elasticsearch & ChromaDB) + +--- + +## πŸš€ What's Next +- **Pre-built Experience Libraries**: Domain repositories (Finance/Coding/Education/Research) + community marketplace +- **Rich Experience Formats**: Executable code/tool configs/pipeline templates/workflows +- **Experience Validation**: Quality analysis + cross-task effectiveness + auto-refinement +- **Universal Trajectory Extraction**: Raw logs/multimodal data/execution traces β†’ experiences + +--- + +## 🌟 What is ExperienceMaker? +ExperienceMaker is a framework that transforms how AI agents learn and improve through **experience-driven intelligence**. +By automatically extracting, storing, and intelligently reusing experiences from agent trajectories, it enables continuous learning and progressive skill enhancement. + +### ✨ Core Capabilities + +#### πŸ” **Intelligent Experience Summarizer** +- **Success Pattern Recognition**: Identify what works and understand the underlying principles +- **Failure Analysis**: Learn from mistakes to avoid repeating them in future tasks +- **Comparative Insights**: Understand the critical differences between successful and failed approaches +- **Multistep Trajectory Processing**: Break down complex tasks into learnable, actionable segments + +#### 🎯 **Smart Experience Retriever** +- **Semantic Search**: Find relevant experiences using advanced embedding models and semantic understanding +- **Context-Aware Ranking**: Prioritize the most applicable experiences for current task contexts +- **Dynamic Rewriting**: Intelligently adapt experiences to fit new situations and requirements +- **Multi-modal Support**: Handle various input types including query, messages + +#### πŸ—„οΈ **Scalable Experience Management** +- **Multiple Storage Backends**: Choose from Elasticsearch (production-ready), ChromaDB (development), or file-based storage (testing) +- **Workspace Isolation**: Organize experiences by projects, domains, or teams with complete separation +- **Deduplication & Validation**: Ensure high-quality, unique experience storage with automated quality control +- **Batch Operations**: Efficiently handle large-scale experience processing with optimized performance + +#### πŸ”§ **Developer-Friendly Architecture** +- **REST API Interface**: Seamless integration with existing systems through clean API design +- **Modular Pipeline Design**: Compose custom workflows from atomic operations with maximum flexibility +- **Flexible Configuration**: YAML files and command-line overrides for easy customization +- **Experience Store**: Ready-to-use out of the box β€” there’s no need for you to manually summarize experiences. You can directly leverage existing, comprehensive experience datasets to greatly enhance your agent’s capabilities. +

+ ExperienceMaker Architecture +

+ +--- + +## πŸ› οΈ Installation + +### Option 1: Install from PyPI (Recommended) + +```bash +pip install experiencemaker +``` + +### Option 2: Install from Source + +```bash +git clone https://github.com/modelscope/ExperienceMaker.git +cd ExperienceMaker +pip install . +``` + +## βš™οΈ Environment Setup + +Create a `.env` file in your project root directory: + +```bash +# Required: LLM API configuration +LLM_API_KEY="sk-xxx" +LLM_BASE_URL="https://xxx.com/v1" + +# Required: Embedding model configuration +EMBEDDING_MODEL_API_KEY="sk-xxx" +EMBEDDING_MODEL_BASE_URL="https://xxx.com/v1" + +# Optional: Elasticsearch configuration (if using Elasticsearch backend) + +``` + +## πŸš€ Quick Start + +### 🌐 HTTP Service + +For testing and development, use the `local_file` backend: +```bash +experiencemaker \ + http_service.port=8001 \ + llm.default.model_name=qwen3-32b \ + embedding_model.default.model_name=text-embedding-v4 \ + vector_store.default.backend=local_file +``` + +πŸ’‘ **Pro Tip**: Check out our [Configuration Guide](./doc/configuration_guide.md) for detailed configuration topics +including custom pipelines, operation parameters, and advanced configuration methods. + +The service will start on `http://localhost:8001` + +### πŸ”Œ MCP Server + +ExperienceMaker now supports Model Context Protocol (MCP) for seamless integration with MCP-compatible clients like Claude Desktop: + +```bash +experiencemaker_mcp \ + mcp_transport=stdio \ + llm.default.model_name=qwen3-32b \ + embedding_model.default.model_name=text-embedding-v4 \ + vector_store.default.backend=local_file +``` + +For SSE transport (Server-Sent Events): +```bash +experiencemaker_mcp \ + mcp_transport=sse \ + http_service.port=8001 \ + llm.default.model_name=qwen3-32b \ + embedding_model.default.model_name=text-embedding-v4 \ + vector_store.default.backend=local_file +``` + +πŸ”— **For detailed MCP setup and usage examples**, see our [MCP Quick Start Guide](./doc/mcp_quick_start.md). + +### πŸ” Production Setup with Elasticsearch Backend +```bash +experiencemaker \ + http_service.port=8001 \ + llm.default.model_name=qwen3-32b \ + embedding_model.default.model_name=text-embedding-v4 \ + vector_store.default.backend=elasticsearch +``` + +**Setup Elasticsearch:** +```bash +export ES_HOSTS="http://localhost:9200" +# Quick setup using Elastic's official script +curl -fsSL https://elastic.co/start-local | sh +``` +πŸ“– **Need Help?** Refer to [Vector Store Setup](./doc/vector_store_setup.md) for comprehensive deployment guidance. + +## πŸ“ Your First ExperienceMaker Script + +Here's how to get started! +Note the `workspace_id` serves as your experience storage namespace. Experiences in different workspaces remain completely isolated and cannot access each other. + +### πŸ“Š Call Summarizer Examples + +Transform conversation trajectories into valuable experiences using batch summarization. Each trajectory contains: + +- **Message**: Complete conversation history between user and agent +- **Score**: Performance rating (0-1 scale, where 0=failure, 1=success) + +The summarizer analyzes these trajectories to extract actionable insights and patterns for future interactions. + +
+Python + +```python +import requests + +response = requests.post(url="http://0.0.0.0:8001/summarizer", json={ + "workspace_id": "test_workspace", + "traj_list": [ + {"messages": [{"role": "user", "content": "hello world"}], "score": 1.0} + ] +}) + +experience_list = response.json()["experience_list"] +for experience in experience_list: + print(experience) +``` +
+ +
+curl + +```bash +curl -X POST "http://0.0.0.0:8001/summarizer" \ + -H "Content-Type: application/json" \ + -d '{ + "workspace_id": "test_workspace", + "traj_list": [ + { + "messages": [{"role": "user", "content": "hello world"}], + "score": 1.0 + } + ] + }' +``` +
+ +
+Node.js + +```javascript +const fetch = require('node-fetch'); +// or: import fetch from 'node-fetch'; + +async function callSummarizer() { + try { + const response = await fetch('http://0.0.0.0:8001/summarizer', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + workspace_id: "test_workspace", + traj_list: [ + { + messages: [{ role: "user", content: "hello world" }], + score: 1.0 + } + ] + }) + }); + + const data = await response.json(); + const experienceList = data.experience_list; + + experienceList.forEach(experience => { + console.log(experience); + }); + } catch (error) { + console.error('Error:', error); + } +} + +callSummarizer(); +``` +
+ +### πŸ” Call Retriever Examples + +Intelligently search and retrieve the most relevant experiences from your workspace to enhance decision-making. The retriever: + +- **Finds** the top-k most similar experiences based on semantic similarity to your query +- **Returns** pre-assembled context ready for immediate use, or raw experience data for custom processing +- **Leverages** your workspace's accumulated knowledge to provide contextually relevant insights + +
+Python + +```python +import requests + +response = requests.post(url="http://0.0.0.0:8001/retriever", json={ + "workspace_id": "test_workspace", + "query": "what is the meaning of life?", + "top_k": 1, +}) + +experience_merged: str = response.json()["experience_merged"] +print(f"experience_merged={experience_merged}") +``` +
+ +
+curl + +```bash +curl -X POST "http://0.0.0.0:8001/retriever" \ + -H "Content-Type: application/json" \ + -d '{ + "workspace_id": "test_workspace", + "query": "what is the meaning of life?", + "top_k": 1 + }' +``` +
+ +
+Node.js + +```javascript +const fetch = require('node-fetch'); +// or: import fetch from 'node-fetch'; + +async function callRetriever() { + try { + const response = await fetch('http://0.0.0.0:8001/retriever', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + workspace_id: "test_workspace", + query: "what is the meaning of life?", + top_k: 1 + }) + }); + + const data = await response.json(); + const experienceMerged = data.experience_merged; + + console.log(`experience_merged=${experienceMerged}`); + } catch (error) { + console.error('Error:', error); + } +} + +callRetriever(); +``` +
+ +### πŸ’Ύ Dump Experiences From Vector Store + +Export and backup your valuable experience data for archival, analysis, or migration purposes. This operation: + +- **Extracts** all experiences from the specified workspace in the vector store +- **Saves** them to a structured JSONL file at `{path}/{workspace_id}.jsonl` +- **Preserves** complete experience metadata and embeddings for future restoration + +
+Python + +```python +import requests + +response = requests.post(url="http://0.0.0.0:8001/vector_store", json={ + "workspace_id": "test_workspace", + "action": "dump", + "path": "./", +}) +print(response.json()) +``` +
+ +
+curl + +```bash +curl -X POST "http://0.0.0.0:8001/vector_store" \ + -H "Content-Type: application/json" \ + -d '{ + "workspace_id": "test_workspace", + "action": "dump", + "path": "./" + }' +``` +
+ +
+Node.js + +```javascript +const fetch = require('node-fetch'); +// or: import fetch from 'node-fetch'; + +async function dumpExperiences() { + try { + const response = await fetch('http://0.0.0.0:8001/vector_store', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + workspace_id: "test_workspace", + action: "dump", + path: "./" + }) + }); + + const data = await response.json(); + console.log(data); + } catch (error) { + console.error('Error:', error); + } +} + +dumpExperiences(); +``` +
+ +### πŸ“₯ Load Experiences To Vector Store + +Import and restore previously exported experience data to populate your workspace with existing knowledge. This operation: + +- **Reads** experience data from the JSONL file located at `{path}/{workspace_id}.jsonl` +- **Reconstructs** the vector embeddings and indexes them in the specified workspace +- **Enables** immediate access to imported experiences for retrieval and decision-making + +
+Python + +```python +import requests + +response = requests.post(url="http://0.0.0.0:8001/vector_store", json={ + "workspace_id": "test_workspace", + "action": "load", + "path": "./", +}) + +print(response.json()) +``` +
+ +
+curl + +```bash +curl -X POST "http://0.0.0.0:8001/vector_store" \ + -H "Content-Type: application/json" \ + -d '{ + "workspace_id": "test_workspace", + "action": "load", + "path": "./" + }' +``` +
+ +
+Node.js + +```javascript +const fetch = require('node-fetch'); +// or: import fetch from 'node-fetch'; + +async function loadExperiences() { + try { + const response = await fetch('http://0.0.0.0:8001/vector_store', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + workspace_id: "test_workspace", + action: "load", + path: "./" + }) + }); + + const data = await response.json(); + console.log(data); + } catch (error) { + console.error('Error:', error); + } +} + +loadExperiences(); +``` +
+ +πŸ’‘ **Need More Advanced Operations?** For additional workspace management features(e.g. delete_workspace, +copy_workspace), advanced configuration options, and troubleshooting guidance, check out our +comprehensive [Quick Start Guide](./cookbook/simple_demo/quick_start.md). + +🎭 **Want to See It in Action?** We've prepared a [simple react agent](./cookbook/simple_demo/simple_demo.py) that demonstrates how to enhance agent capabilities by integrating summarizer and retriever components, achieving significantly better performance. + +--- + +## πŸ§ͺ Experiments + +### 🌍 Experiment on Appworld + +We test ExperienceMaker on Appworld with qwen3-8b: + +| Method | pass@1 | pass@2 | pass@4 | +|--------------------------------|-----------|-------------|-----------| +| w/o ExperienceMaker (baseline) | 0.083 | 0.140 | 0.228 | +| **w ExperienceMaker** | | | | +|experience(Direct Use) | **0.109** | **0.175** | **0.281** | + +Pass@K measures the probability that at least one out of K generated samples successfully completes the task (achieves score=1). +The current experiments use an internal AppWorld environment which may have slight discrepancies, and we will soon update with experimental results from the standard AppWorld environment. + +You may find more details to reproduce this experiment in [quickstart.md](cookbook/appworld/quickstart.md) + + +### 🧊 Experiment on Frozenlake + +| without experience | with experience | +|:-------------------------------------------------------------------------------------------:|:-------------------------------------------:| +|

GIF 1

|

GIF 2

+ +We test on 100 random frozenlake map with qwen3-8b: + +| Method | pass rate | +|-------------------------------|------------------| +| w/o ExperienceMaker (baseline) | 0.66 | +| **w ExperienceMaker** | | +| [1] experience(Direct Use) | 0.72 **(+9.1%)** | +| [2] experience(LLM Rewritten) | 0.72 **(+9.1%)** | + +We also noticed that in such simple scenarios, not using LLM rewriting may actually yield better results. + +Therefore, in some simple scenarios, you can also try disabling LLM rewriting by simply changing the following in default_config.yaml: + +```yaml +rewrite_experience_op: + params: + enable_llm_rewrite: false # change this to false +``` + +You may find more details to reproduce this experiment in [quickstart.md](cookbook/frozenlake/quickstart.md) + +### πŸ”§ Experiment on BFCL-V3 + +Coming Soon! Stay tuned for comprehensive evaluation results. + +--- + +## πŸͺ Ready-made Experience Store + +ExperienceMaker provides pre-built experience libraries to jumpstart your agent's capabilities. +You can directly load these curated experiences into your workspace and start benefiting from accumulated knowledge +immediately. + +### πŸ“¦ Available Experience Libraries + +- **`appworld_v1.jsonl`**: Comprehensive experiences from Appworld agent interactions, covering complex task planning + and execution patterns +- **`bfcl_v1.jsonl`**: Function calling experiences from Berkeley Function-Calling Leaderboard tasks + +### πŸš€ Quick Start with Pre-built Experiences + +Here's how to load and use the Appworld experience library: + +#### Step 1: Load Pre-built Experiences + +
+Python + +```python +import requests + +# Load Appworld experiences into your workspace +response = requests.post(url="http://0.0.0.0:8001/vector_store", json={ + "workspace_id": "appworld_v1", + "action": "load", + "path": "./library/", +}) + +print(f"loading result result={response.json()}") +``` +
+ +
+curl + +```bash +curl -X POST "http://0.0.0.0:8001/vector_store" \ + -H "Content-Type: application/json" \ + -d '{ + "workspace_id": "appworld_v1", + "action": "load", + "path": "./library/" + }' +``` +
+ +#### Step 2: Retrieve Relevant Experiences + +Now you can query the loaded experiences to get contextual guidance for your tasks: + +
+Python + +```python +import requests + +# Query for app interaction experiences +response = requests.post(url="http://0.0.0.0:8001/retriever", json={ + "workspace_id": "appworld_v1", + "query": "How to navigate to settings and update user profile information?", + "top_k": 1, +}) + +experience_merged = response.json()["experience_merged"] +print(f"Retrieved experiences: {experience_merged}") +``` +
+ +
+curl + +```bash +curl -X POST "http://0.0.0.0:8001/retriever" \ + -H "Content-Type: application/json" \ + -d '{ + "workspace_id": "appworld_v1", + "query": "How to navigate to settings and update user profile information?", + "top_k": 1 + }' +``` +
+ +--- + +## πŸ“š Additional Resources + +- **[Quick Start](./cookbook/simple_demo/quick_start.md)**: This guide will help you get started with ExperienceMaker quickly using practical examples. +- **[Vector Store Setup](./doc/vector_store_setup.md)**: Complete production deployment guide +- **[Configuration Guide](./doc/configuration_guide.md)**: Describes all available command-line parameters for ExperienceMaker Service +- **[Operations Documentation](./doc/operations_documentation.md)**: Comprehensive operations configuration reference +- **[Example Collection](./cookbook)**: Practical examples and use cases +- **[Future RoadMap](./doc/future_roadmap.md)**: Our vision and upcoming features + +--- + +## 🀝 Contributing +We warmly welcome contributions from the community! Here's how you can help make ExperienceMaker even better: + +### πŸ› **Report Issues** +- Bug reports with detailed reproduction steps +- Feature requests and enhancement suggestions +- Documentation improvements and clarifications +- Performance optimization ideas + +### πŸ’» **Code Contributions** +- New operations and tools development +- Backend implementations and optimizations +- API enhancements and new endpoints +- Test coverage improvements and quality assurance + +### πŸ“ **Documentation** +- Usage examples and comprehensive tutorials +- Best practices guides and design patterns +- Translation and localization efforts + +--- +## πŸ“„ Citation +If you use ExperienceMaker in your research or projects, please cite: +```bibtex +@software{ExperienceMaker, + title = {ExperienceMaker: A Comprehensive Framework for AI Agent Experience Generation and Reuse}, + author = {The ExperienceMaker Team}, + url = {https://github.com/modelscope/ExperienceMaker}, + month = {08}, + year = {2025}, +} +``` + +--- +## βš–οΈ License +This project is licensed under the Apache License 2.0 - see the [LICENSE](./LICENSE) file for details. + +--- diff --git a/experiencemaker/ExperienceMaker.egg-info/SOURCES.txt b/experiencemaker/ExperienceMaker.egg-info/SOURCES.txt new file mode 100644 index 00000000..bd1253cb --- /dev/null +++ b/experiencemaker/ExperienceMaker.egg-info/SOURCES.txt @@ -0,0 +1,103 @@ +LICENSE +README.md +pyproject.toml +ExperienceMaker.egg-info/PKG-INFO +ExperienceMaker.egg-info/SOURCES.txt +ExperienceMaker.egg-info/dependency_links.txt +ExperienceMaker.egg-info/entry_points.txt +ExperienceMaker.egg-info/requires.txt +ExperienceMaker.egg-info/top_level.txt +experiencemaker/__init__.py +experiencemaker/app.py +experiencemaker/mcp_server.py +experiencemaker/config/__init__.py +experiencemaker/config/config_parser.py +experiencemaker/config/default_config.yaml +experiencemaker/config/mock_config.yaml +experiencemaker/config/simple_config.yaml +experiencemaker/embedding_model/__init__.py +experiencemaker/embedding_model/base_embedding_model.py +experiencemaker/embedding_model/openai_compatible_embedding_model.py +experiencemaker/enumeration/__init__.py +experiencemaker/enumeration/agent_state.py +experiencemaker/enumeration/chunk_enum.py +experiencemaker/enumeration/http_enum.py +experiencemaker/enumeration/role.py +experiencemaker/llm/__init__.py +experiencemaker/llm/base_llm.py +experiencemaker/llm/openai_compatible_llm.py +experiencemaker/op/__init__.py +experiencemaker/op/base_op.py +experiencemaker/op/mock_op.py +experiencemaker/op/prompt_mixin.py +experiencemaker/op/react/__init__.py +experiencemaker/op/react/react_v1_op.py +experiencemaker/op/react/react_v1_prompt.yaml +experiencemaker/op/retriever/__init__.py +experiencemaker/op/retriever/build_query_op.py +experiencemaker/op/retriever/build_query_prompt.yaml +experiencemaker/op/retriever/merge_experience_op.py +experiencemaker/op/retriever/rerank_experience_op.py +experiencemaker/op/retriever/rerank_experience_prompt.yaml +experiencemaker/op/retriever/rewrite_experience_op.py +experiencemaker/op/retriever/rewrite_experience_prompt.yaml +experiencemaker/op/summarizer/__init__.py +experiencemaker/op/summarizer/comparative_extraction_op.py +experiencemaker/op/summarizer/comparative_extraction_prompt.yaml +experiencemaker/op/summarizer/experience_deduplication_op.py +experiencemaker/op/summarizer/experience_validation_op.py +experiencemaker/op/summarizer/experience_validation_prompt.yaml +experiencemaker/op/summarizer/failure_extraction_op.py +experiencemaker/op/summarizer/failure_extraction_prompt.yaml +experiencemaker/op/summarizer/pdf_preprocess_op.py +experiencemaker/op/summarizer/simple_comparative_summary_op.py +experiencemaker/op/summarizer/simple_comparative_summary_prompt.yaml +experiencemaker/op/summarizer/simple_summary_op.py +experiencemaker/op/summarizer/simple_summary_prompt.yaml +experiencemaker/op/summarizer/success_extraction_op.py +experiencemaker/op/summarizer/success_extraction_prompt.yaml +experiencemaker/op/summarizer/trajectory_preprocess_op.py +experiencemaker/op/summarizer/trajectory_segmentation_op.py +experiencemaker/op/summarizer/trajectory_segmentation_prompt.yaml +experiencemaker/op/vector_store/__init__.py +experiencemaker/op/vector_store/recall_vector_store_op.py +experiencemaker/op/vector_store/update_vector_store_op.py +experiencemaker/op/vector_store/vector_store_action_op.py +experiencemaker/pipeline/__init__.py +experiencemaker/pipeline/pipeline.py +experiencemaker/pipeline/pipeline_context.py +experiencemaker/schema/__init__.py +experiencemaker/schema/app_config.py +experiencemaker/schema/experience.py +experiencemaker/schema/message.py +experiencemaker/schema/request.py +experiencemaker/schema/response.py +experiencemaker/schema/vector_node.py +experiencemaker/service/__init__.py +experiencemaker/service/experience_maker_client.py +experiencemaker/service/experience_maker_service.py +experiencemaker/service/mcp_client.py +experiencemaker/tool/__init__.py +experiencemaker/tool/base_tool.py +experiencemaker/tool/code_tool.py +experiencemaker/tool/dashscope_search_tool.py +experiencemaker/tool/mcp_tool.py +experiencemaker/tool/tavily_search_tool.py +experiencemaker/tool/terminate_tool.py +experiencemaker/utils/__init__.py +experiencemaker/utils/common_utils.py +experiencemaker/utils/file_handler.py +experiencemaker/utils/http_client.py +experiencemaker/utils/op_utils.py +experiencemaker/utils/registry.py +experiencemaker/utils/singleton.py +experiencemaker/utils/timer.py +experiencemaker/vector_store/__init__.py +experiencemaker/vector_store/base_vector_store.py +experiencemaker/vector_store/chroma_vector_store.py +experiencemaker/vector_store/es_vector_store.py +experiencemaker/vector_store/file_vector_store.py +test/test1.py +test/test2.py +test/test3.py +test/test4.py \ No newline at end of file diff --git a/experiencemaker/ExperienceMaker.egg-info/dependency_links.txt b/experiencemaker/ExperienceMaker.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/experiencemaker/ExperienceMaker.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/experiencemaker/ExperienceMaker.egg-info/entry_points.txt b/experiencemaker/ExperienceMaker.egg-info/entry_points.txt new file mode 100644 index 00000000..0095d037 --- /dev/null +++ b/experiencemaker/ExperienceMaker.egg-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +experiencemaker = experiencemaker.app:main +experiencemaker_mcp = experiencemaker.mcp_server:main diff --git a/experiencemaker/ExperienceMaker.egg-info/requires.txt b/experiencemaker/ExperienceMaker.egg-info/requires.txt new file mode 100644 index 00000000..e794d28c --- /dev/null +++ b/experiencemaker/ExperienceMaker.egg-info/requires.txt @@ -0,0 +1,13 @@ +dashscope>=1.19.1 +elasticsearch>=8.14.0 +fastapi>=0.115.13 +fastmcp>=2.10.6 +loguru>=0.7.3 +mcp>=1.9.4 +numpy>=2.3.0 +openai>=1.88.0 +pydantic>=2.11.7 +PyYAML>=6.0.2 +Requests>=2.32.4 +uvicorn>=0.34.3 +setuptools>=75.0 diff --git a/experiencemaker/ExperienceMaker.egg-info/top_level.txt b/experiencemaker/ExperienceMaker.egg-info/top_level.txt new file mode 100644 index 00000000..07626190 --- /dev/null +++ b/experiencemaker/ExperienceMaker.egg-info/top_level.txt @@ -0,0 +1 @@ +experiencemaker diff --git a/experiencemaker/cookbook/bfcl/bfcl_agent.py b/experiencemaker/cookbook/bfcl/bfcl_agent.py index d48e1ad5..b134fd05 100644 --- a/experiencemaker/cookbook/bfcl/bfcl_agent.py +++ b/experiencemaker/cookbook/bfcl/bfcl_agent.py @@ -63,6 +63,7 @@ class BFCLAgent: num_runs: int = 1, enable_thinking: bool = False, use_experience: bool = False, + use_fixed_experience: bool = True, experience_base_url: str = "http://0.0.0.0:8001/", experience_workspace_id: str = "bfcl_8b_0725"): @@ -79,6 +80,7 @@ class BFCLAgent: self.num_runs: int = num_runs self.enable_thinking: bool = enable_thinking self.use_experience: bool = use_experience + self.use_fixed_experience: bool = use_fixed_experience self.experience_base_url: str = experience_base_url self.experience_workspace_id: str = experience_workspace_id @@ -135,11 +137,11 @@ class BFCLAgent: def update_experience(self, trajectories): response = requests.post(url=self.experience_base_url + "summarizer", json={ "workspace_id": self.experience_workspace_id, - "trajectories": trajectories, + "traj_list": trajectories, }) response.raise_for_status() response = response.json() - return response["experiences"] + print(f"add new experiences: {response["experience_list"]}") def call_llm(self, messages: list, tool_schemas: list[dict]) -> str: for i in range(100): @@ -517,9 +519,12 @@ class BFCLAgent: break reward = self.get_reward(run_id, task_index) - # if reward == 1: - # - # self.update_experience([process_msg_to_trajectory(task_id, msg, reward)]) # selectively add experiences when succeed + if reward == 1 and not self.use_fixed_experience: + self.update_experience([{ + "task_id":task_id, + "messages":self.history[run_id][task_index], + "score":reward + }]) # selectively add experiences when succeed t_result = { "run_id": run_id, diff --git a/experiencemaker/cookbook/bfcl/run_bfcl.py b/experiencemaker/cookbook/bfcl/run_bfcl.py index 82a8d0e7..8796dcd8 100644 --- a/experiencemaker/cookbook/bfcl/run_bfcl.py +++ b/experiencemaker/cookbook/bfcl/run_bfcl.py @@ -22,6 +22,7 @@ def run_agent(dataset_name: str, data_path: str = "data/multiturn_data_base_val.jsonl", answer_path: Path = Path("data/possible_answer"), use_experience: bool = False, + use_fixed_experience: bool = True, enable_thinking: bool = False, experience_base_url: str = "http://0.0.0.0:8001/", experience_workspace_id: str = "bfcl_8b_0725"): @@ -51,6 +52,7 @@ def run_agent(dataset_name: str, model_name=model_name, num_runs=num_runs, use_experience=use_experience, + use_fixed_experience=use_fixed_experience, enable_thinking=enable_thinking, experience_base_url=experience_base_url, experience_workspace_id=experience_workspace_id @@ -82,6 +84,7 @@ def run_agent(dataset_name: str, answer_path=answer_path, enable_thinking=enable_thinking, use_experience=use_experience, + use_fixed_experience=use_fixed_experience, experience_base_url=experience_base_url, experience_workspace_id=experience_workspace_id) task_results = agent.execute() @@ -95,6 +98,7 @@ def main(): max_workers = 4 num_runs = 4 # Run each task 4 times use_experience = True + use_fixed_experience = True experience_base_url = "http://0.0.0.0:8001/" experience_workspace_id = "bfcl_v1" if max_workers > 1: @@ -102,14 +106,15 @@ def main(): for run_id in range(num_runs): run_agent( dataset_name="bfcl-multi-turn-base-val", - experiment_suffix=f"0812-w-exp-extract-compare-recall", + experiment_suffix=f"0813-w-exp-w-think-update-test", model_name="qwen3-8b", max_workers=max_workers, num_runs=1, data_path="data/multiturn_data_base_val.jsonl", answer_path=Path("data/possible_answer"), - enable_thinking=False, + enable_thinking=True, use_experience=use_experience, + use_fixed_experience=use_fixed_experience, experience_base_url=experience_base_url, experience_workspace_id=experience_workspace_id, ) diff --git a/experiencemaker/cookbook/bfcl/run_exp_statistic.py b/experiencemaker/cookbook/bfcl/run_exp_statistic.py index cee2af85..2231004f 100644 --- a/experiencemaker/cookbook/bfcl/run_exp_statistic.py +++ b/experiencemaker/cookbook/bfcl/run_exp_statistic.py @@ -61,7 +61,7 @@ def get_possible_k_values(total_runs: int) -> list: def run_exp_statistic(): - path: Path = Path(f"./no_exp_result/qwen3-8b") + path: Path = Path(f"./exp_result/qwen3-8b") # Store results for all experiments all_results = {} diff --git a/experiencemaker/experiencemaker/op/manager/__init__.py b/experiencemaker/experiencemaker/op/manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/experiencemaker/experiencemaker/op/manager/experience_deletion_op.py b/experiencemaker/experiencemaker/op/manager/experience_deletion_op.py new file mode 100644 index 00000000..681ad2bb --- /dev/null +++ b/experiencemaker/experiencemaker/op/manager/experience_deletion_op.py @@ -0,0 +1,162 @@ +from typing import List +from loguru import logger + +from experiencemaker.op import OP_REGISTRY +from experiencemaker.op.base_op import BaseOp +from experiencemaker.schema.experience import BaseExperience +from experiencemaker.schema.request import ManagerRequest + + +@OP_REGISTRY.register() +class ExperienceDeletionOp(BaseOp): + current_path: str = __file__ + + def execute(self): + """Remove low-utility experiences""" + request: ManagerRequest = self.context.request + experiences: List[BaseExperience] = self.context.response.experience_list + + if not experiences: + logger.info("No experiences found for deduplication") + return + + logger.info(f"Starting deduplication for {len(experiences)} experiences") + + # Perform deduplication + deduplicated_experiences = self._deduplicate_experiences(experiences) + + logger.info(f"Deduplication complete: {len(deduplicated_experiences)} deduplicated experiences out of {len(experiences)}") + + # Update context + self.context.response.experience_list = deduplicated_experiences + + def _deduplicate_experiences(self, experiences: List[BaseExperience]) -> List[BaseExperience]: + """Remove duplicate experiences""" + if not experiences: + return experiences + + similarity_threshold = self.op_params.get("similarity_threshold", 0.5) + workspace_id = self.context.request.workspace_id if hasattr(self.context, 'request') else None + + unique_experiences = [] + + # Get existing experience embeddings + existing_embeddings = self._get_existing_experience_embeddings(workspace_id) + + for experience in experiences: + # Generate embedding for current experience + current_embedding = self._get_experience_embedding(experience) + + if current_embedding is None: + logger.warning(f"Failed to generate embedding for experience: {str(experience.when_to_use)[:50]}...") + continue + + # Check similarity with existing experiences + if self._is_similar_to_existing_experiences(current_embedding, existing_embeddings, similarity_threshold): + logger.debug(f"Skipping similar experience: {str(experience.when_to_use)[:50]}...") + continue + + # Check similarity with current batch experiences + if self._is_similar_to_current_experiences(current_embedding, unique_experiences, similarity_threshold): + logger.debug(f"Skipping duplicate in current batch: {str(experience.when_to_use)[:50]}...") + continue + + # Add to unique experiences list + unique_experiences.append(experience) + logger.debug(f"Added unique experience: {str(experience.when_to_use)[:50]}...") + + return unique_experiences + + def _get_existing_experience_embeddings(self, workspace_id: str) -> List[List[float]]: + """Get embeddings of existing experiences""" + try: + if not hasattr(self, 'vector_store') or not self.vector_store or not workspace_id: + return [] + + # Query existing experience nodes + existing_nodes = self.vector_store.search( + query="...", # Empty query to get all + workspace_id=workspace_id, + top_k=self.op_params.get("max_existing_experiences", 1000) + ) + + # Extract embeddings + existing_embeddings = [] + for node in existing_nodes: + if hasattr(node, 'embedding') and node.embedding: + existing_embeddings.append(node.embedding) + + logger.debug(f"Retrieved {len(existing_embeddings)} existing experience embeddings from workspace {workspace_id}") + return existing_embeddings + + except Exception as e: + logger.warning(f"Failed to retrieve existing experience embeddings: {e}") + return [] + + def _get_experience_embedding(self, experience: BaseExperience) -> List[float]: + """Generate embedding for experience""" + try: + if not hasattr(self, 'vector_store') or not self.vector_store: + return None + + # Combine experience description and content for embedding + text_for_embedding = f"{experience.when_to_use} {experience.content}" + embeddings = self.vector_store.embedding_model.get_embeddings([text_for_embedding]) + + if embeddings and len(embeddings) > 0: + return embeddings[0] + else: + logger.warning("Empty embedding generated for experience") + return None + + except Exception as e: + logger.error(f"Error generating embedding for experience: {e}") + return None + + + def _is_similar_to_existing_experiences(self, current_embedding: List[float], + existing_embeddings: List[List[float]], + threshold: float) -> bool: + """Check if current embedding is similar to existing embeddings""" + for existing_embedding in existing_embeddings: + similarity = self._calculate_cosine_similarity(current_embedding, existing_embedding) + if similarity > threshold: + logger.debug(f"Found similar existing experience with similarity: {similarity:.3f}") + return True + return False + + def _is_similar_to_current_experiences(self, current_embedding: List[float], + current_experiences: List[BaseExperience], + threshold: float) -> bool: + for existing_experience in current_experiences: + existing_embedding = self._get_experience_embedding(existing_experience) + if existing_embedding is None: + continue + + similarity = self._calculate_cosine_similarity(current_embedding, existing_embedding) + if similarity > threshold: + logger.debug(f"Found similar experience in current batch with similarity: {similarity:.3f}") + return True + return False + + def _calculate_cosine_similarity(self, embedding1: List[float], embedding2: List[float]) -> float: + """Calculate cosine similarity""" + try: + import numpy as np + + vec1 = np.array(embedding1) + vec2 = np.array(embedding2) + + # Calculate cosine similarity + dot_product = np.dot(vec1, vec2) + norm1 = np.linalg.norm(vec1) + norm2 = np.linalg.norm(vec2) + + if norm1 == 0 or norm2 == 0: + return 0.0 + + return dot_product / (norm1 * norm2) + + except Exception as e: + logger.error(f"Error calculating cosine similarity: {e}") + return 0.0 \ No newline at end of file diff --git a/experiencemaker/experiencemaker/op/vector_store/recall_vector_store_op.py b/experiencemaker/experiencemaker/op/vector_store/recall_vector_store_op.py index a92eef03..07ee355d 100644 --- a/experiencemaker/experiencemaker/op/vector_store/recall_vector_store_op.py +++ b/experiencemaker/experiencemaker/op/vector_store/recall_vector_store_op.py @@ -25,7 +25,7 @@ class RecallVectorStoreOp(BaseOp): nodes: List[VectorNode] = self.vector_store.search(query=query, workspace_id=request.workspace_id, top_k=request.top_k) - + # convert to experience, filter duplicate experience_list: List[BaseExperience] = [] experience_content_list: List[str] = [] diff --git a/experiencemaker/experiencemaker/schema/request.py b/experiencemaker/experiencemaker/schema/request.py index 4e35df87..3f5980fa 100644 --- a/experiencemaker/experiencemaker/schema/request.py +++ b/experiencemaker/experiencemaker/schema/request.py @@ -20,6 +20,11 @@ class SummarizerRequest(BaseRequest): traj_list: List[Trajectory] = Field(default_factory=list) +class ManagerRequest(BaseRequest): + freq_threshold: int = Field(default=10) + utility_threshold: float = Field(default=0.6) + + class VectorStoreRequest(BaseRequest): action: str = Field(default="") src_workspace_id: str = Field(default="")