Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +28 -0
- .gitignore +62 -0
- .gitmodules +3 -0
- CODE_OF_CONDUCT.md +45 -0
- CODE_OF_CONDUCT_CN.md +48 -0
- CONTRIBUTING.md +200 -0
- CONTRIBUTING_CN.md +201 -0
- LICENSE +22 -0
- MANIFEST.in +5 -0
- README.md +344 -0
- README_CN.md +345 -0
- docs/DEVELOPMENT_GUIDE.md +417 -0
- docs/DEVELOPMENT_GUIDE_CN.md +420 -0
- docs/PRODUCTION_GUIDE.md +162 -0
- docs/PRODUCTION_GUIDE_CN.md +161 -0
- docs/assets/demo1-task-execution.gif +3 -0
- docs/assets/demo2-claude-skill.gif +3 -0
- docs/assets/demo3-web-search.gif +3 -0
- examples/01_basic_tools.py +137 -0
- examples/02_simple_agent.py +212 -0
- examples/03_session_notes.py +250 -0
- examples/04_full_agent.py +276 -0
- examples/05_provider_selection.py +190 -0
- examples/06_tool_schema_demo.py +285 -0
- examples/README.md +219 -0
- examples/README_CN.md +219 -0
- mini_agent/__init__.py +17 -0
- mini_agent/acp/__init__.py +197 -0
- mini_agent/acp/server.py +6 -0
- mini_agent/agent.py +523 -0
- mini_agent/cli.py +834 -0
- mini_agent/config.py +220 -0
- mini_agent/config/config-example.yaml +60 -0
- mini_agent/config/mcp-example.json +29 -0
- mini_agent/config/system_prompt.md +75 -0
- mini_agent/llm/__init__.py +9 -0
- mini_agent/llm/anthropic_client.py +293 -0
- mini_agent/llm/base.py +84 -0
- mini_agent/llm/llm_wrapper.py +127 -0
- mini_agent/llm/openai_client.py +295 -0
- mini_agent/logger.py +178 -0
- mini_agent/retry.py +138 -0
- mini_agent/schema/__init__.py +19 -0
- mini_agent/schema/schema.py +55 -0
- mini_agent/skills/.claude-plugin/marketplace.json +43 -0
- mini_agent/skills/.gitignore +2 -0
- mini_agent/skills/README.md +123 -0
- mini_agent/skills/THIRD_PARTY_NOTICES.md +405 -0
- mini_agent/skills/agent_skills_spec.md +55 -0
- mini_agent/skills/algorithmic-art/LICENSE.txt +202 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,31 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
docs/assets/demo1-task-execution.gif filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
docs/assets/demo2-claude-skill.gif filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
docs/assets/demo3-web-search.gif filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
mini_agent/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
mini_agent/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
mini_agent/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
mini_agent/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
mini_agent/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
mini_agent/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
mini_agent/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
mini_agent/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
mini_agent/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
mini_agent/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
mini_agent/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
mini_agent/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
mini_agent/skills/canvas-design/canvas-fonts/Jura-Light.ttf filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
mini_agent/skills/canvas-design/canvas-fonts/Jura-Medium.ttf filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
mini_agent/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
mini_agent/skills/canvas-design/canvas-fonts/Lora-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
mini_agent/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
mini_agent/skills/canvas-design/canvas-fonts/Lora-Italic.ttf filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
mini_agent/skills/canvas-design/canvas-fonts/Lora-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
mini_agent/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
mini_agent/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
mini_agent/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
mini_agent/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
mini_agent/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
mini_agent/skills/theme-factory/theme-showcase.pdf filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual environments
|
| 24 |
+
venv/
|
| 25 |
+
ENV/
|
| 26 |
+
env/
|
| 27 |
+
.venv
|
| 28 |
+
|
| 29 |
+
# IDE
|
| 30 |
+
.vscode/
|
| 31 |
+
.idea/
|
| 32 |
+
*.swp
|
| 33 |
+
*.swo
|
| 34 |
+
*~
|
| 35 |
+
|
| 36 |
+
# Workspace
|
| 37 |
+
workspace/
|
| 38 |
+
*.log
|
| 39 |
+
|
| 40 |
+
# OS
|
| 41 |
+
.DS_Store
|
| 42 |
+
Thumbs.db
|
| 43 |
+
|
| 44 |
+
# Config (contains API keys)
|
| 45 |
+
config.yaml
|
| 46 |
+
mcp.json
|
| 47 |
+
|
| 48 |
+
# test file
|
| 49 |
+
demo.py
|
| 50 |
+
hello.py
|
| 51 |
+
test.txt
|
| 52 |
+
hello_world.py
|
| 53 |
+
demo_test.txt
|
| 54 |
+
demo_commands.txt
|
| 55 |
+
mcp.json.bak
|
| 56 |
+
|
| 57 |
+
docs/assets/backup/
|
| 58 |
+
docs/assets/preview.html
|
| 59 |
+
|
| 60 |
+
claude.md
|
| 61 |
+
|
| 62 |
+
.agent_memory.json
|
.gitmodules
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[submodule "mini_agent/skills"]
|
| 2 |
+
path = mini_agent/skills
|
| 3 |
+
url = https://github.com/anthropics/skills.git
|
CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Code of Conduct
|
| 2 |
+
|
| 3 |
+
## Our Pledge
|
| 4 |
+
|
| 5 |
+
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
| 6 |
+
|
| 7 |
+
## Our Standards
|
| 8 |
+
|
| 9 |
+
Examples of behavior that contributes to creating a positive environment include:
|
| 10 |
+
|
| 11 |
+
- Using welcoming and inclusive language
|
| 12 |
+
- Being respectful of differing viewpoints and experiences
|
| 13 |
+
- Gracefully accepting constructive criticism
|
| 14 |
+
- Focusing on what is best for the community
|
| 15 |
+
- Showing empathy towards other community members
|
| 16 |
+
|
| 17 |
+
Examples of unacceptable behavior by participants include:
|
| 18 |
+
|
| 19 |
+
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
| 20 |
+
- Trolling, insulting/derogatory comments, and personal or political attacks
|
| 21 |
+
- Public or private harassment
|
| 22 |
+
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
| 23 |
+
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
| 24 |
+
|
| 25 |
+
## Our Responsibilities
|
| 26 |
+
|
| 27 |
+
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
| 28 |
+
|
| 29 |
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
| 30 |
+
|
| 31 |
+
## Scope
|
| 32 |
+
|
| 33 |
+
This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
| 34 |
+
|
| 35 |
+
## Enforcement
|
| 36 |
+
|
| 37 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
| 38 |
+
|
| 39 |
+
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
| 40 |
+
|
| 41 |
+
## Attribution
|
| 42 |
+
|
| 43 |
+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
| 44 |
+
|
| 45 |
+
For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
CODE_OF_CONDUCT_CN.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 行为准则
|
| 2 |
+
|
| 3 |
+
## 我们的承诺
|
| 4 |
+
|
| 5 |
+
为了营造开放和友好的环境,我们作为贡献者和维护者承诺:无论年龄、体型、残疾、种族、性别认同和表达、经验水平、教育程度、社会经济地位、国籍、外貌、种族、宗教或性取向如何,参与我们的项目和社区的每个人都不会受到骚扰。
|
| 6 |
+
|
| 7 |
+
## 我们的标准
|
| 8 |
+
|
| 9 |
+
有助于创建积极环境的行为包括:
|
| 10 |
+
|
| 11 |
+
- 使用友好和包容的语言
|
| 12 |
+
- 尊重不同的观点和经验
|
| 13 |
+
- 优雅地接受建设性批评
|
| 14 |
+
- 专注于对社区最有利的事情
|
| 15 |
+
- 对其他社区成员表示同理心
|
| 16 |
+
|
| 17 |
+
不可接受的行为包括:
|
| 18 |
+
|
| 19 |
+
- 使用性暗示的语言或图像,以及不受欢迎的性关注或挑逗
|
| 20 |
+
- 发表侮辱性/贬损性评论,进行人身攻击或政治攻击
|
| 21 |
+
- 公开或私下骚扰
|
| 22 |
+
- 未经明确许可,发布他人的私人信息(如地址、电子邮件地址)
|
| 23 |
+
- 在专业环境中可被合理认为不适当的其他行为
|
| 24 |
+
|
| 25 |
+
## 我们的责任
|
| 26 |
+
|
| 27 |
+
项目维护者有责任澄清可接受行为的标准,并应对任何不可接受的行为采取适当和公平的纠正措施。
|
| 28 |
+
|
| 29 |
+
项目维护者有权利和责任删除、编辑或拒绝不符合本行为准则的评论、提交、代码、wiki 编辑、问题和其他贡献,或暂时或永久禁止任何他们认为有不适当、威胁、冒犯或有害行为的贡献者。
|
| 30 |
+
|
| 31 |
+
## 范围
|
| 32 |
+
|
| 33 |
+
本行为准则适用于项目空间和公共空间,当个人代表项目或其社区时。代表项目或社区的示例包括:使用官方项目电子邮件地址、通过官方社交媒体账户发帖,或在在线或离线活动中担任指定代表。项目维护者可以进一步定义和阐明项目的代表性。
|
| 34 |
+
|
| 35 |
+
## 执行
|
| 36 |
+
|
| 37 |
+
可以通过联系项目团队来报告滥用、骚扰或其他不可接受的行为。所有投诉都将被审查和调查,并将做出被认为必要和适当的回应。项目团队有义务对事件报告者保密。具体执行政策的更多细节可能会单独发布。
|
| 38 |
+
|
| 39 |
+
不善意遵守或执行行为准则的项目维护者可能会面临项目领导层其他成员决定的临时或永久性后果。
|
| 40 |
+
|
| 41 |
+
## 归属
|
| 42 |
+
|
| 43 |
+
本行为准则改编自 [Contributor Covenant](https://www.contributor-covenant.org/) 2.0 版本,可在 https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 获取。
|
| 44 |
+
|
| 45 |
+
社区影响指南受到 [Mozilla 的行为准则执行阶梯](https://github.com/mozilla/diversity)的启发。
|
| 46 |
+
|
| 47 |
+
有关本行为准则的常见问题的答案,请参阅 https://www.contributor-covenant.org/faq。翻译版本可在 https://www.contributor-covenant.org/translations 获取。
|
| 48 |
+
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing Guide
|
| 2 |
+
|
| 3 |
+
Thank you for your interest in the Mini Agent project! We welcome contributions of all forms.
|
| 4 |
+
|
| 5 |
+
## How to Contribute
|
| 6 |
+
|
| 7 |
+
### Reporting Bugs
|
| 8 |
+
|
| 9 |
+
If you find a bug, please create an Issue and include the following information:
|
| 10 |
+
|
| 11 |
+
- **Problem Description**: A clear description of the problem.
|
| 12 |
+
- **Steps to Reproduce**: Detailed steps to reproduce the issue.
|
| 13 |
+
- **Expected Behavior**: What you expected to happen.
|
| 14 |
+
- **Actual Behavior**: What actually happened.
|
| 15 |
+
- **Environment Information**:
|
| 16 |
+
- Python version
|
| 17 |
+
- Operating system
|
| 18 |
+
- Versions of relevant dependencies
|
| 19 |
+
|
| 20 |
+
### Suggesting New Features
|
| 21 |
+
|
| 22 |
+
If you have an idea for a new feature, please create an Issue first to discuss it:
|
| 23 |
+
|
| 24 |
+
- Describe the purpose and value of the feature.
|
| 25 |
+
- Explain the intended use case.
|
| 26 |
+
- Provide a design proposal if possible.
|
| 27 |
+
|
| 28 |
+
### Submitting Code
|
| 29 |
+
|
| 30 |
+
#### Getting Started
|
| 31 |
+
|
| 32 |
+
1. Fork this repository.
|
| 33 |
+
2. Clone your fork:
|
| 34 |
+
```bash
|
| 35 |
+
git clone https://github.com/MiniMax-AI/Mini-Agent mini-agent
|
| 36 |
+
cd mini-agent
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
3. Create a new branch:
|
| 40 |
+
```bash
|
| 41 |
+
git checkout -b feature/your-feature-name
|
| 42 |
+
# or
|
| 43 |
+
git checkout -b fix/your-bug-fix
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
4. Install development dependencies:
|
| 47 |
+
```bash
|
| 48 |
+
uv sync
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
#### Development Process
|
| 52 |
+
|
| 53 |
+
1. **Write Code**
|
| 54 |
+
- Follow the project's code style (see the [Development Guide](docs/DEVELOPMENT.md#code-style-guide)).
|
| 55 |
+
- Add necessary comments and docstrings.
|
| 56 |
+
- Keep your code clean and concise.
|
| 57 |
+
|
| 58 |
+
2. **Add Tests**
|
| 59 |
+
- Add test cases for new features.
|
| 60 |
+
- Ensure all tests pass:
|
| 61 |
+
```bash
|
| 62 |
+
pytest tests/ -v
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
3. **Update Documentation**
|
| 66 |
+
- If you add a new feature, update the README or relevant documentation.
|
| 67 |
+
- Keep documentation in sync with your code.
|
| 68 |
+
|
| 69 |
+
4. **Commit Changes**
|
| 70 |
+
- Use clear commit messages:
|
| 71 |
+
```bash
|
| 72 |
+
git commit -m "feat(tools): Add new file search tool"
|
| 73 |
+
# or
|
| 74 |
+
git commit -m "fix(agent): Fix error handling for tool calls"
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
- Commit message format:
|
| 78 |
+
- `feat`: A new feature
|
| 79 |
+
- `fix`: A bug fix
|
| 80 |
+
- `docs`: Documentation updates
|
| 81 |
+
- `style`: Code style adjustments
|
| 82 |
+
- `refactor`: Code refactoring
|
| 83 |
+
- `test`: Test-related changes
|
| 84 |
+
- `chore`: Build or auxiliary tools
|
| 85 |
+
|
| 86 |
+
5. **Push to Your Fork**
|
| 87 |
+
```bash
|
| 88 |
+
git push origin feature/your-feature-name
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
6. **Create a Pull Request**
|
| 92 |
+
- Create a Pull Request on GitHub.
|
| 93 |
+
- Clearly describe your changes.
|
| 94 |
+
- Reference any related Issues if applicable.
|
| 95 |
+
|
| 96 |
+
#### Pull Request Checklist
|
| 97 |
+
|
| 98 |
+
Before submitting a PR, please ensure:
|
| 99 |
+
|
| 100 |
+
- [ ] The code follows the project's style guide.
|
| 101 |
+
- [ ] All tests pass.
|
| 102 |
+
- [ ] Necessary tests have been added.
|
| 103 |
+
- [ ] Relevant documentation has been updated.
|
| 104 |
+
- [ ] The commit message is clear and concise.
|
| 105 |
+
- [ ] There are no unrelated changes.
|
| 106 |
+
|
| 107 |
+
### Code Review
|
| 108 |
+
|
| 109 |
+
All Pull Requests will be reviewed:
|
| 110 |
+
|
| 111 |
+
- We will review your code as soon as possible.
|
| 112 |
+
- We may request some changes.
|
| 113 |
+
- Please be patient and responsive to feedback.
|
| 114 |
+
- Once approved, your PR will be merged into the main branch.
|
| 115 |
+
|
| 116 |
+
## Code Style Guide
|
| 117 |
+
|
| 118 |
+
### Python Code Style
|
| 119 |
+
|
| 120 |
+
Follow PEP 8 and the Google Python Style Guide:
|
| 121 |
+
|
| 122 |
+
```python
|
| 123 |
+
# Good example ✅
|
| 124 |
+
class MyClass:
|
| 125 |
+
"""A brief description of the class.
|
| 126 |
+
|
| 127 |
+
A more detailed description...
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
def my_method(self, param1: str, param2: int = 10) -> str:
|
| 131 |
+
"""A brief description of the method.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
param1: Description of parameter 1.
|
| 135 |
+
param2: Description of parameter 2.
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
Description of the return value.
|
| 139 |
+
"""
|
| 140 |
+
pass
|
| 141 |
+
|
| 142 |
+
# Bad example ❌
|
| 143 |
+
class myclass: # Class names should be PascalCase
|
| 144 |
+
def MyMethod(self,param1,param2=10): # Method names should be snake_case
|
| 145 |
+
pass # Missing docstring
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### Type Hinting
|
| 149 |
+
|
| 150 |
+
Use Python type hints:
|
| 151 |
+
|
| 152 |
+
```python
|
| 153 |
+
from typing import List, Dict, Optional, Any
|
| 154 |
+
|
| 155 |
+
async def process_messages(
|
| 156 |
+
messages: List[Dict[str, Any]],
|
| 157 |
+
max_tokens: Optional[int] = None
|
| 158 |
+
) -> str:
|
| 159 |
+
"""Process a list of messages."""
|
| 160 |
+
pass
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
### Testing
|
| 164 |
+
|
| 165 |
+
- Write tests for new features.
|
| 166 |
+
- Keep tests simple and clear.
|
| 167 |
+
- Ensure tests cover critical paths.
|
| 168 |
+
|
| 169 |
+
```python
|
| 170 |
+
import pytest
|
| 171 |
+
from mini_agent.tools.my_tool import MyTool
|
| 172 |
+
|
| 173 |
+
@pytest.mark.asyncio
|
| 174 |
+
async def test_my_tool():
|
| 175 |
+
"""Test the custom tool."""
|
| 176 |
+
tool = MyTool()
|
| 177 |
+
result = await tool.execute(param="test")
|
| 178 |
+
assert result.success
|
| 179 |
+
assert "expected" in result.content
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
## Community Guidelines
|
| 183 |
+
|
| 184 |
+
Please follow our [Code of Conduct](CODE_OF_CONDUCT.md) and be friendly and respectful.
|
| 185 |
+
|
| 186 |
+
## Questions and Help
|
| 187 |
+
|
| 188 |
+
If you have any questions:
|
| 189 |
+
|
| 190 |
+
- Check the [README](README.md) and [documentation](docs/).
|
| 191 |
+
- Search existing Issues.
|
| 192 |
+
- Create a new Issue to ask a question.
|
| 193 |
+
|
| 194 |
+
## License
|
| 195 |
+
|
| 196 |
+
By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
Thank you again for your contribution! 🎉
|
CONTRIBUTING_CN.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 贡献指南
|
| 2 |
+
|
| 3 |
+
感谢你对 Mini Agent 项目的兴趣!我们欢迎各种形式的贡献。
|
| 4 |
+
|
| 5 |
+
## 如何贡献
|
| 6 |
+
|
| 7 |
+
### 报告 Bug
|
| 8 |
+
|
| 9 |
+
如果你发现了 bug,请创建一个 Issue 并包含以下信息:
|
| 10 |
+
|
| 11 |
+
- **问题描述**:清晰描述问题
|
| 12 |
+
- **复现步骤**:详细的复现步骤
|
| 13 |
+
- **预期行为**:你期望发生什么
|
| 14 |
+
- **实际行为**:实际发生了什么
|
| 15 |
+
- **环境信息**:
|
| 16 |
+
- Python 版本
|
| 17 |
+
- 操作系统
|
| 18 |
+
- 相关依赖版本
|
| 19 |
+
|
| 20 |
+
### 提出新功能
|
| 21 |
+
|
| 22 |
+
如果你有新功能的想法,请先创建一个 Issue 讨论:
|
| 23 |
+
|
| 24 |
+
- 描述功能的用途和价值
|
| 25 |
+
- 说明预期的使用场景
|
| 26 |
+
- 如果可能,提供设计思路
|
| 27 |
+
|
| 28 |
+
### 提交代码
|
| 29 |
+
|
| 30 |
+
#### 准备工作
|
| 31 |
+
|
| 32 |
+
1. Fork 本仓库
|
| 33 |
+
2. 克隆你的 fork:
|
| 34 |
+
```bash
|
| 35 |
+
git clone https://github.com/MiniMax-AI/Mini-Agent mini-agent
|
| 36 |
+
cd mini-agent
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
3. 创建新分支:
|
| 40 |
+
```bash
|
| 41 |
+
git checkout -b feature/your-feature-name
|
| 42 |
+
# 或
|
| 43 |
+
git checkout -b fix/your-bug-fix
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
4. 安装开发依赖:
|
| 47 |
+
```bash
|
| 48 |
+
uv sync
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
#### 开发流程
|
| 52 |
+
|
| 53 |
+
1. **编写代码**
|
| 54 |
+
- 遵循项目的代码风格(参考 [开发指南](docs/DEVELOPMENT.md#代码规范))
|
| 55 |
+
- 添加必要的注释和文档字符串
|
| 56 |
+
- 保持代码简洁清晰
|
| 57 |
+
|
| 58 |
+
2. **添加测试**
|
| 59 |
+
- 为新功能添加测试用例
|
| 60 |
+
- 确保所有测试通过:
|
| 61 |
+
```bash
|
| 62 |
+
pytest tests/ -v
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
3. **更新文档**
|
| 66 |
+
- 如果添加了新功能,更新 README 或相关文档
|
| 67 |
+
- 保持文档与代码同步
|
| 68 |
+
|
| 69 |
+
4. **提交更改**
|
| 70 |
+
- 使用清晰的提交消息:
|
| 71 |
+
```bash
|
| 72 |
+
git commit -m "feat(tools): 添加新的文件搜索工具"
|
| 73 |
+
# 或
|
| 74 |
+
git commit -m "fix(agent): 修复工具调用错误处理"
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
- 提交消息格式:
|
| 78 |
+
- `feat`: 新功能
|
| 79 |
+
- `fix`: Bug 修复
|
| 80 |
+
- `docs`: 文档更新
|
| 81 |
+
- `style`: 代码格式调整
|
| 82 |
+
- `refactor`: 代码重构
|
| 83 |
+
- `test`: 测试相关
|
| 84 |
+
- `chore`: 构建或辅助工具
|
| 85 |
+
|
| 86 |
+
5. **推送到你的 fork**
|
| 87 |
+
```bash
|
| 88 |
+
git push origin feature/your-feature-name
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
6. **创建 Pull Request**
|
| 92 |
+
- 在 GitHub 上创建 Pull Request
|
| 93 |
+
- 清楚描述你的更改
|
| 94 |
+
- 引用相关的 Issue(如果有)
|
| 95 |
+
|
| 96 |
+
#### Pull Request 检查清单
|
| 97 |
+
|
| 98 |
+
在提交 PR 之前,请确保:
|
| 99 |
+
|
| 100 |
+
- [ ] 代码遵循项目规范
|
| 101 |
+
- [ ] 所有测试通过
|
| 102 |
+
- [ ] 添加了必要的测试
|
| 103 |
+
- [ ] 更新了相关文档
|
| 104 |
+
- [ ] 提交消息清晰明确
|
| 105 |
+
- [ ] 没有不相关的更改
|
| 106 |
+
|
| 107 |
+
### 代码审查
|
| 108 |
+
|
| 109 |
+
所有 Pull Request 需要经过代码审查:
|
| 110 |
+
|
| 111 |
+
- 我们会尽快审查你的代码
|
| 112 |
+
- 可能会要求一些修改
|
| 113 |
+
- 请保持耐心并及时响应反馈
|
| 114 |
+
- 审查通过后会被合并到主分支
|
| 115 |
+
|
| 116 |
+
## 代码规范
|
| 117 |
+
|
| 118 |
+
### Python 代码风格
|
| 119 |
+
|
| 120 |
+
遵循 PEP 8 和 Google Python Style Guide:
|
| 121 |
+
|
| 122 |
+
```python
|
| 123 |
+
# 好的示例 ✅
|
| 124 |
+
class MyClass:
|
| 125 |
+
"""类的简短描述。
|
| 126 |
+
|
| 127 |
+
详细描述...
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
def my_method(self, param1: str, param2: int = 10) -> str:
|
| 131 |
+
"""方法的简短描述。
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
param1: 参数1的描述
|
| 135 |
+
param2: 参数2的描述
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
返回值的描述
|
| 139 |
+
"""
|
| 140 |
+
pass
|
| 141 |
+
|
| 142 |
+
# 不好的示例 ❌
|
| 143 |
+
class myclass: # 类名应该用 PascalCase
|
| 144 |
+
def MyMethod(self,param1,param2=10): # 方法名应该用 snake_case
|
| 145 |
+
pass # 缺少 docstring
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### 类型注解
|
| 149 |
+
|
| 150 |
+
使用 Python 类型注解:
|
| 151 |
+
|
| 152 |
+
```python
|
| 153 |
+
from typing import List, Dict, Optional
|
| 154 |
+
|
| 155 |
+
async def process_messages(
|
| 156 |
+
messages: List[Dict[str, Any]],
|
| 157 |
+
max_tokens: Optional[int] = None
|
| 158 |
+
) -> str:
|
| 159 |
+
"""处理消息列表"""
|
| 160 |
+
pass
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
### 测试
|
| 164 |
+
|
| 165 |
+
- 为新功能编写测试
|
| 166 |
+
- 保持测试简单清晰
|
| 167 |
+
- 测试覆盖关键路径
|
| 168 |
+
|
| 169 |
+
```python
|
| 170 |
+
import pytest
|
| 171 |
+
from mini_agent.tools.my_tool import MyTool
|
| 172 |
+
|
| 173 |
+
@pytest.mark.asyncio
|
| 174 |
+
async def test_my_tool():
|
| 175 |
+
"""测试自定义工具"""
|
| 176 |
+
tool = MyTool()
|
| 177 |
+
result = await tool.execute(param="test")
|
| 178 |
+
assert result.success
|
| 179 |
+
assert "expected" in result.content
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
## 社区准则
|
| 183 |
+
|
| 184 |
+
请遵守我们的[行为准则](CODE_OF_CONDUCT.md),保持友好和尊重。
|
| 185 |
+
|
| 186 |
+
## 问题和帮助
|
| 187 |
+
|
| 188 |
+
如果有任何问题:
|
| 189 |
+
|
| 190 |
+
- 查看 [README](README.md) 和 [文档](docs/)
|
| 191 |
+
- 搜索现有的 Issues
|
| 192 |
+
- 创建新的 Issue 提问
|
| 193 |
+
|
| 194 |
+
## 许可证
|
| 195 |
+
|
| 196 |
+
提交代码即表示你同意将代码以 [MIT License](LICENSE) 发布。
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
再次感谢你的贡献! 🎉
|
| 201 |
+
|
LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 MiniMax
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
| 22 |
+
|
MANIFEST.in
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
recursive-include mini_agent/skills *
|
| 2 |
+
include mini_agent/skills/README.md
|
| 3 |
+
include mini_agent/skills/THIRD_PARTY_NOTICES.md
|
| 4 |
+
include mini_agent/skills/agent_skills_spec.md
|
| 5 |
+
|
README.md
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Mini Agent
|
| 2 |
+
|
| 3 |
+
English | [中文](./README_CN.md)
|
| 4 |
+
|
| 5 |
+
**Mini Agent** is a minimal yet professional demo project that showcases the best practices for building agents with the MiniMax M2.1 model. Leveraging an Anthropic-compatible API, it fully supports interleaved thinking to unlock M2's powerful reasoning capabilities for long, complex tasks.
|
| 6 |
+
|
| 7 |
+
This project comes packed with features designed for a robust and intelligent agent development experience:
|
| 8 |
+
|
| 9 |
+
* ✅ **Full Agent Execution Loop**: A complete and reliable foundation with a basic toolset for file system and shell operations.
|
| 10 |
+
* ✅ **Persistent Memory**: An active **Session Note Tool** ensures the agent retains key information across multiple sessions.
|
| 11 |
+
* ✅ **Intelligent Context Management**: Automatically summarizes conversation history to handle contexts up to a configurable token limit, enabling infinitely long tasks.
|
| 12 |
+
* ✅ **Claude Skills Integration**: Comes with 15 professional skills for documents, design, testing, and development.
|
| 13 |
+
* ✅ **MCP Tool Integration**: Natively supports MCP for tools like knowledge graph access and web search.
|
| 14 |
+
* ✅ **Comprehensive Logging**: Detailed logs for every request, response, and tool execution for easy debugging.
|
| 15 |
+
* ✅ **Clean & Simple Design**: A beautiful CLI and a codebase that is easy to understand, making it the perfect starting point for building advanced agents.
|
| 16 |
+
|
| 17 |
+
## Table of Contents
|
| 18 |
+
|
| 19 |
+
- [Mini Agent](#mini-agent)
|
| 20 |
+
- [Table of Contents](#table-of-contents)
|
| 21 |
+
- [Quick Start](#quick-start)
|
| 22 |
+
- [1. Get API Key](#1-get-api-key)
|
| 23 |
+
- [2. Choose Your Usage Mode](#2-choose-your-usage-mode)
|
| 24 |
+
- [🚀 Quick Start Mode (Recommended for Beginners)](#-quick-start-mode-recommended-for-beginners)
|
| 25 |
+
- [🔧 Development Mode](#-development-mode)
|
| 26 |
+
- [ACP \& Zed Editor Integration(optional)](#acp--zed-editor-integrationoptional)
|
| 27 |
+
- [Usage Examples](#usage-examples)
|
| 28 |
+
- [Task Execution](#task-execution)
|
| 29 |
+
- [Using a Claude Skill (e.g., PDF Generation)](#using-a-claude-skill-eg-pdf-generation)
|
| 30 |
+
- [Web Search \& Summarization (MCP Tool)](#web-search--summarization-mcp-tool)
|
| 31 |
+
- [Testing](#testing)
|
| 32 |
+
- [Quick Run](#quick-run)
|
| 33 |
+
- [Test Coverage](#test-coverage)
|
| 34 |
+
- [Troubleshooting](#troubleshooting)
|
| 35 |
+
- [SSL Certificate Error](#ssl-certificate-error)
|
| 36 |
+
- [Module Not Found Error](#module-not-found-error)
|
| 37 |
+
- [Related Documentation](#related-documentation)
|
| 38 |
+
- [Community](#community)
|
| 39 |
+
- [Contributing](#contributing)
|
| 40 |
+
- [License](#license)
|
| 41 |
+
- [References](#references)
|
| 42 |
+
|
| 43 |
+
## Quick Start
|
| 44 |
+
|
| 45 |
+
### 1. Get API Key
|
| 46 |
+
|
| 47 |
+
MiniMax provides both global and China platforms. Choose based on your network environment:
|
| 48 |
+
|
| 49 |
+
| Version | Platform | API Base |
|
| 50 |
+
| ---------- | -------------------------------------------------------------- | -------------------------- |
|
| 51 |
+
| **Global** | [https://platform.minimax.io](https://platform.minimax.io) | `https://api.minimax.io` |
|
| 52 |
+
| **China** | [https://platform.minimaxi.com](https://platform.minimaxi.com) | `https://api.minimaxi.com` |
|
| 53 |
+
|
| 54 |
+
**Steps to get API Key:**
|
| 55 |
+
1. Visit the corresponding platform to register and login
|
| 56 |
+
2. Go to **Account Management > API Keys**
|
| 57 |
+
3. Click **"Create New Key"**
|
| 58 |
+
4. Copy and save it securely (key is only shown once)
|
| 59 |
+
|
| 60 |
+
> 💡 **Tip**: Remember the API Base address corresponding to your chosen platform, you'll need it for configuration
|
| 61 |
+
|
| 62 |
+
### 2. Choose Your Usage Mode
|
| 63 |
+
|
| 64 |
+
**Prerequisites: Install uv**
|
| 65 |
+
|
| 66 |
+
Both usage modes require uv. If you don't have it installed:
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
# macOS/Linux/WSL
|
| 70 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 71 |
+
|
| 72 |
+
# Windows (PowerShell)
|
| 73 |
+
python -m pip install --user pipx
|
| 74 |
+
python -m pipx ensurepath
|
| 75 |
+
# Restart PowerShell after installation
|
| 76 |
+
|
| 77 |
+
# After installation, restart your terminal or run:
|
| 78 |
+
source ~/.bashrc # or ~/.zshrc (macOS/Linux)
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
We offer two usage modes - choose based on your needs:
|
| 82 |
+
|
| 83 |
+
#### 🚀 Quick Start Mode (Recommended for Beginners)
|
| 84 |
+
|
| 85 |
+
Perfect for users who want to quickly try Mini Agent without cloning the repository or modifying code.
|
| 86 |
+
|
| 87 |
+
**Installation:**
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
# 1. Install directly from GitHub
|
| 91 |
+
uv tool install git+https://github.com/MiniMax-AI/Mini-Agent.git
|
| 92 |
+
|
| 93 |
+
# 2. Run setup script (automatically creates config files)
|
| 94 |
+
# macOS/Linux:
|
| 95 |
+
curl -fsSL https://raw.githubusercontent.com/MiniMax-AI/Mini-Agent/main/scripts/setup-config.sh | bash
|
| 96 |
+
|
| 97 |
+
# Windows (PowerShell):
|
| 98 |
+
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/MiniMax-AI/Mini-Agent/main/scripts/setup-config.ps1" -OutFile "$env:TEMP\setup-config.ps1"
|
| 99 |
+
powershell -ExecutionPolicy Bypass -File "$env:TEMP\setup-config.ps1"
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
> 💡 **Tip**: If you want to develop locally or modify code, use "Development Mode" below
|
| 103 |
+
|
| 104 |
+
**Configuration:**
|
| 105 |
+
|
| 106 |
+
The setup script creates config files in `~/.mini-agent/config/`. Edit the config file:
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
# Edit config file
|
| 110 |
+
nano ~/.mini-agent/config/config.yaml
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
Fill in your API Key and corresponding API Base:
|
| 114 |
+
|
| 115 |
+
```yaml
|
| 116 |
+
api_key: "YOUR_API_KEY_HERE" # API Key from step 1
|
| 117 |
+
api_base: "https://api.minimax.io" # Global
|
| 118 |
+
# api_base: "https://api.minimaxi.com" # China
|
| 119 |
+
model: "MiniMax-M2.1"
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
**Start Using:**
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
mini-agent # Use current directory as workspace
|
| 126 |
+
mini-agent --workspace /path/to/your/project # Specify workspace directory
|
| 127 |
+
mini-agent --version # Check version
|
| 128 |
+
|
| 129 |
+
# Management commands
|
| 130 |
+
uv tool upgrade mini-agent # Upgrade to latest version
|
| 131 |
+
uv tool uninstall mini-agent # Uninstall if needed
|
| 132 |
+
uv tool list # View all installed tools
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
#### 🔧 Development Mode
|
| 136 |
+
|
| 137 |
+
For developers who need to modify code, add features, or debug.
|
| 138 |
+
|
| 139 |
+
**Installation & Configuration:**
|
| 140 |
+
|
| 141 |
+
```bash
|
| 142 |
+
# 1. Clone the repository
|
| 143 |
+
git clone https://github.com/MiniMax-AI/Mini-Agent.git
|
| 144 |
+
cd Mini-Agent
|
| 145 |
+
|
| 146 |
+
# 2. Install uv (if you haven't)
|
| 147 |
+
# macOS/Linux:
|
| 148 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 149 |
+
# Windows (PowerShell):
|
| 150 |
+
irm https://astral.sh/uv/install.ps1 | iex
|
| 151 |
+
# Restart terminal after installation
|
| 152 |
+
|
| 153 |
+
# 3. Sync dependencies
|
| 154 |
+
uv sync
|
| 155 |
+
|
| 156 |
+
# Alternative: Install dependencies manually (if not using uv)
|
| 157 |
+
# pip install -r requirements.txt
|
| 158 |
+
# Or install required packages:
|
| 159 |
+
# pip install tiktoken pyyaml httpx pydantic requests prompt-toolkit mcp
|
| 160 |
+
|
| 161 |
+
# 4. Initialize Claude Skills (Optional)
|
| 162 |
+
git submodule update --init --recursive
|
| 163 |
+
|
| 164 |
+
# 5. Copy config template
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
**macOS/Linux:**
|
| 168 |
+
```bash
|
| 169 |
+
cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
**Windows:**
|
| 173 |
+
```powershell
|
| 174 |
+
Copy-Item mini_agent\config\config-example.yaml mini_agent\config\config.yaml
|
| 175 |
+
|
| 176 |
+
# 6. Edit config file
|
| 177 |
+
vim mini_agent/config/config.yaml # Or use your preferred editor
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
Fill in your API Key and corresponding API Base:
|
| 181 |
+
|
| 182 |
+
```yaml
|
| 183 |
+
api_key: "YOUR_API_KEY_HERE" # API Key from step 1
|
| 184 |
+
api_base: "https://api.minimax.io" # Global
|
| 185 |
+
# api_base: "https://api.minimaxi.com" # China
|
| 186 |
+
model: "MiniMax-M2.1"
|
| 187 |
+
max_steps: 100
|
| 188 |
+
workspace_dir: "./workspace"
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
> 📖 Full configuration guide: See [config-example.yaml](mini_agent/config/config-example.yaml)
|
| 192 |
+
|
| 193 |
+
**Run Methods:**
|
| 194 |
+
|
| 195 |
+
Choose your preferred run method:
|
| 196 |
+
|
| 197 |
+
```bash
|
| 198 |
+
# Method 1: Run as module directly (good for debugging)
|
| 199 |
+
uv run python -m mini_agent.cli
|
| 200 |
+
|
| 201 |
+
# Method 2: Install in editable mode (recommended)
|
| 202 |
+
uv tool install -e .
|
| 203 |
+
# After installation, run from anywhere and code changes take effect immediately
|
| 204 |
+
mini-agent
|
| 205 |
+
mini-agent --workspace /path/to/your/project
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
> 📖 For more development guidance, see [Development Guide](docs/DEVELOPMENT_GUIDE.md)
|
| 209 |
+
|
| 210 |
+
> 📖 For more production deployment guidance, see [Production Guide](docs/PRODUCTION_GUIDE.md)
|
| 211 |
+
|
| 212 |
+
## ACP & Zed Editor Integration(optional)
|
| 213 |
+
|
| 214 |
+
Mini Agent supports the [Agent Communication Protocol (ACP)](https://github.com/modelcontextprotocol/protocol) for integration with code editors like Zed.
|
| 215 |
+
|
| 216 |
+
**Setup in Zed Editor:**
|
| 217 |
+
|
| 218 |
+
1. Install Mini Agent in development mode or as a tool
|
| 219 |
+
2. Add to your Zed `settings.json`:
|
| 220 |
+
|
| 221 |
+
```json
|
| 222 |
+
{
|
| 223 |
+
"agent_servers": {
|
| 224 |
+
"mini-agent": {
|
| 225 |
+
"command": "/path/to/mini-agent-acp"
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
The command path should be:
|
| 232 |
+
- If installed via `uv tool install`: Use the output of `which mini-agent-acp`
|
| 233 |
+
- If in development mode: `./mini_agent/acp/server.py`
|
| 234 |
+
|
| 235 |
+
**Usage:**
|
| 236 |
+
- Open Zed's agent panel with `Ctrl+Shift+P` → "Agent: Toggle Panel"
|
| 237 |
+
- Select "mini-agent" from the agent dropdown
|
| 238 |
+
- Start conversations with Mini Agent directly in your editor
|
| 239 |
+
|
| 240 |
+
## Usage Examples
|
| 241 |
+
|
| 242 |
+
Here are a few examples of what Mini Agent can do.
|
| 243 |
+
|
| 244 |
+
### Task Execution
|
| 245 |
+
|
| 246 |
+
*In this demo, the agent is asked to create a simple, beautiful webpage and display it in the browser, showcasing the basic tool-use loop.*
|
| 247 |
+
|
| 248 |
+

|
| 249 |
+
|
| 250 |
+
### Using a Claude Skill (e.g., PDF Generation)
|
| 251 |
+
|
| 252 |
+
*Here, the agent leverages a Claude Skill to create a professional document (like a PDF or DOCX) based on the user's request, demonstrating its advanced capabilities.*
|
| 253 |
+
|
| 254 |
+

|
| 255 |
+
|
| 256 |
+
### Web Search & Summarization (MCP Tool)
|
| 257 |
+
|
| 258 |
+
*This demo shows the agent using its web search tool to find up-to-date information online and summarize it for the user.*
|
| 259 |
+
|
| 260 |
+

|
| 261 |
+
|
| 262 |
+
## Testing
|
| 263 |
+
|
| 264 |
+
The project includes comprehensive test cases covering unit tests, functional tests, and integration tests.
|
| 265 |
+
|
| 266 |
+
### Quick Run
|
| 267 |
+
|
| 268 |
+
```bash
|
| 269 |
+
# Run all tests
|
| 270 |
+
pytest tests/ -v
|
| 271 |
+
|
| 272 |
+
# Run core functionality tests
|
| 273 |
+
pytest tests/test_agent.py tests/test_note_tool.py -v
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
### Test Coverage
|
| 277 |
+
|
| 278 |
+
- ✅ **Unit Tests** - Tool classes, LLM client
|
| 279 |
+
- ✅ **Functional Tests** - Session Note Tool, MCP loading
|
| 280 |
+
- ✅ **Integration Tests** - Agent end-to-end execution
|
| 281 |
+
- ✅ **External Services** - Git MCP Server loading
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
## Troubleshooting
|
| 285 |
+
|
| 286 |
+
### SSL Certificate Error
|
| 287 |
+
|
| 288 |
+
If you encounter `[SSL: CERTIFICATE_VERIFY_FAILED]` error:
|
| 289 |
+
|
| 290 |
+
**Quick fix for testing** (modify `mini_agent/llm.py`):
|
| 291 |
+
```python
|
| 292 |
+
# Line 50: Add verify=False to AsyncClient
|
| 293 |
+
async with httpx.AsyncClient(timeout=120.0, verify=False) as client:
|
| 294 |
+
```
|
| 295 |
+
|
| 296 |
+
**Production solution**:
|
| 297 |
+
```bash
|
| 298 |
+
# Update certificates
|
| 299 |
+
pip install --upgrade certifi
|
| 300 |
+
|
| 301 |
+
# Or configure system proxy/certificates
|
| 302 |
+
```
|
| 303 |
+
|
| 304 |
+
### Module Not Found Error
|
| 305 |
+
|
| 306 |
+
Make sure you're running from the project directory:
|
| 307 |
+
```bash
|
| 308 |
+
cd Mini-Agent
|
| 309 |
+
python -m mini_agent.cli
|
| 310 |
+
```
|
| 311 |
+
|
| 312 |
+
## Related Documentation
|
| 313 |
+
|
| 314 |
+
- [Development Guide](docs/DEVELOPMENT_GUIDE.md) - Detailed development and configuration guidance
|
| 315 |
+
- [Production Guide](docs/PRODUCTION_GUIDE.md) - Best practices for production deployment
|
| 316 |
+
|
| 317 |
+
## Community
|
| 318 |
+
|
| 319 |
+
Join the MiniMax official community to get help, share ideas, and stay updated:
|
| 320 |
+
|
| 321 |
+
- **WeChat Group**: Scan the QR code on [Contact Us](https://platform.minimaxi.com/docs/faq/contact-us) page to join
|
| 322 |
+
|
| 323 |
+
## Contributing
|
| 324 |
+
|
| 325 |
+
Issues and Pull Requests are welcome!
|
| 326 |
+
|
| 327 |
+
- [Contributing Guide](CONTRIBUTING.md) - How to contribute
|
| 328 |
+
- [Code of Conduct](CODE_OF_CONDUCT.md) - Community guidelines
|
| 329 |
+
|
| 330 |
+
## License
|
| 331 |
+
|
| 332 |
+
This project is licensed under the [MIT License](LICENSE).
|
| 333 |
+
|
| 334 |
+
## References
|
| 335 |
+
|
| 336 |
+
- MiniMax API: https://platform.minimaxi.com/document
|
| 337 |
+
- MiniMax-M2: https://github.com/MiniMax-AI/MiniMax-M2
|
| 338 |
+
- Anthropic API: https://docs.anthropic.com/claude/reference
|
| 339 |
+
- Claude Skills: https://github.com/anthropics/skills
|
| 340 |
+
- MCP Servers: https://github.com/modelcontextprotocol/servers
|
| 341 |
+
|
| 342 |
+
---
|
| 343 |
+
|
| 344 |
+
**⭐ If this project helps you, please give it a Star!**
|
README_CN.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Mini Agent
|
| 2 |
+
|
| 3 |
+
[English](./README.md) | 中文
|
| 4 |
+
|
| 5 |
+
**Mini Agent** 是一个极简但专业的演示项目,旨在展示使用 MiniMax M2.1 模型构建 Agent 的最佳实践。项目通过兼容 Anthropic 的 API,完全支持交错思维(interleaved thinking),从而解锁 M2 模型在处理长而复杂的任务时强大的推理能力。
|
| 6 |
+
|
| 7 |
+
该项目具备一系列为稳健、智能的 Agent 开发而设计的特性:
|
| 8 |
+
|
| 9 |
+
* ✅ **完整的 Agent 执行循环**:一个完整可靠的执行框架,配备了文件系统和 Shell 操作的基础工具集。
|
| 10 |
+
* ✅ **持久化记忆**:通过内置的 **Session Note Tool**,Agent 能够在多个会话中保留关键信息。
|
| 11 |
+
* ✅ **智能上下文管理**:自动对会话历史进行摘要,可处理长达可配置 Token 上限的上下文,从而支持无限长的任务。
|
| 12 |
+
* ✅ **集成 Claude Skills**:内置 15 种专业技能,涵盖文档处理、设计、测试和开发等领域。
|
| 13 |
+
* ✅ **集成 MCP 工具**:原生支持 MCP 协议,可轻松接入知识图谱、网页搜索等工具。
|
| 14 |
+
* ✅ **全面的日志记录**:为每个请求、响应和工具执行提供详细日志,便于调试。
|
| 15 |
+
* ✅ **简洁明了的设计**:美观的命令行界面和易于理解的代码库,使其成为构建高级 Agent 的理想起点。
|
| 16 |
+
|
| 17 |
+
## 目录
|
| 18 |
+
|
| 19 |
+
- [Mini Agent](#mini-agent)
|
| 20 |
+
- [目录](#目录)
|
| 21 |
+
- [快速开始](#快速开始)
|
| 22 |
+
- [1. 获取 API Key](#1-获取-api-key)
|
| 23 |
+
- [2. 选择使用模式](#2-选择使用模式)
|
| 24 |
+
- [🚀 快速上手模式(推荐新手)](#-快速上手模式推荐新手)
|
| 25 |
+
- [🔧 开发模式](#-开发模式)
|
| 26 |
+
- [ACP \& Zed Editor 集成(可选)](#acp--zed-editor-集成可选)
|
| 27 |
+
- [使用示例](#使用示例)
|
| 28 |
+
- [任务执行](#任务执行)
|
| 29 |
+
- [使用 Claude Skill(例如:PDF 生成)](#使用-claude-skill例如pdf-生成)
|
| 30 |
+
- [网页搜索与摘要(MCP 工具)](#网页搜索与摘要mcp-工具)
|
| 31 |
+
- [测试](#测试)
|
| 32 |
+
- [快速运行](#快速运行)
|
| 33 |
+
- [测试覆盖范围](#测试覆盖范围)
|
| 34 |
+
- [常见问题](#常见问题)
|
| 35 |
+
- [SSL 证书错误](#ssl-证书错误)
|
| 36 |
+
- [模块未找到错误](#模块未找到错误)
|
| 37 |
+
- [相关文档](#相关文档)
|
| 38 |
+
- [社区](#社区)
|
| 39 |
+
- [贡献](#贡献)
|
| 40 |
+
- [许可证](#许可证)
|
| 41 |
+
- [参考资源](#参考资源)
|
| 42 |
+
|
| 43 |
+
## 快速开始
|
| 44 |
+
|
| 45 |
+
### 1. 获取 API Key
|
| 46 |
+
|
| 47 |
+
MiniMax 提供国内和海外两个平台,请根据您的网络环境选择:
|
| 48 |
+
|
| 49 |
+
| 版本 | 平台地址 | API Base |
|
| 50 |
+
| ---------- | -------------------------------------------------------------- | -------------------------- |
|
| 51 |
+
| **国内版** | [https://platform.minimaxi.com](https://platform.minimaxi.com) | `https://api.minimaxi.com` |
|
| 52 |
+
| **海外版** | [https://platform.minimax.io](https://platform.minimax.io) | `https://api.minimax.io` |
|
| 53 |
+
|
| 54 |
+
**获取步骤:**
|
| 55 |
+
1. 访问相应平台注册并登录
|
| 56 |
+
2. 进入 **账户管理 > API 密钥**
|
| 57 |
+
3. 点击 **"创建新密钥"**
|
| 58 |
+
4. 复制并妥善保存(密钥仅显示一次)
|
| 59 |
+
|
| 60 |
+
> 💡 **提示**:请记住您所选平台对应的 API Base 地址,后续配置时会用到。
|
| 61 |
+
|
| 62 |
+
### 2. 选择使用模式
|
| 63 |
+
|
| 64 |
+
**前置要求:安装 uv**
|
| 65 |
+
|
| 66 |
+
两种使用模式都需要 uv。如果您尚未安装:
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
# macOS/Linux/WSL
|
| 70 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 71 |
+
|
| 72 |
+
# Windows (PowerShell)
|
| 73 |
+
python -m pip install --user pipx
|
| 74 |
+
python -m pipx ensurepath
|
| 75 |
+
# 安装后需要重启 PowerShell
|
| 76 |
+
|
| 77 |
+
# 安装完成后,重启终端或运行:
|
| 78 |
+
source ~/.bashrc # 或 ~/.zshrc (macOS/Linux)
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
我们提供两种使用模式,请根据您的需求选择:
|
| 82 |
+
|
| 83 |
+
#### 🚀 快速上手模式(推荐新手)
|
| 84 |
+
|
| 85 |
+
此模式适合希望快速体验 Mini Agent,而无需克隆代码仓库或修改代码的用户。
|
| 86 |
+
|
| 87 |
+
**安装步骤:**
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
# 1. 直接从 GitHub 安装
|
| 91 |
+
uv tool install git+https://github.com/MiniMax-AI/Mini-Agent.git
|
| 92 |
+
|
| 93 |
+
# 2. 运行配置脚本(自动创建配置文件)
|
| 94 |
+
# macOS/Linux:
|
| 95 |
+
curl -fsSL https://raw.githubusercontent.com/MiniMax-AI/Mini-Agent/main/scripts/setup-config.sh | bash
|
| 96 |
+
|
| 97 |
+
# Windows (PowerShell):
|
| 98 |
+
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/MiniMax-AI/Mini-Agent/main/scripts/setup-config.ps1" -OutFile "$env:TEMP\setup-config.ps1"
|
| 99 |
+
powershell -ExecutionPolicy Bypass -File "$env:TEMP\setup-config.ps1"
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
> 💡 **提示**:如果您希望在本地进行开发或修改代码,请使用下方的"开发模式"。
|
| 103 |
+
|
| 104 |
+
**配置步骤:**
|
| 105 |
+
|
| 106 |
+
配置脚本会在 `~/.mini-agent/config/` 目录下创建配置文件,请编辑该文件:
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
# 编辑配置文件
|
| 110 |
+
nano ~/.mini-agent/config/config.yaml
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
填入您的 API Key 和对应的 API Base:
|
| 114 |
+
|
| 115 |
+
```yaml
|
| 116 |
+
api_key: "YOUR_API_KEY_HERE" # 填入第 1 步获取的 API Key
|
| 117 |
+
api_base: "https://api.minimaxi.com" # 国内版
|
| 118 |
+
# api_base: "https://api.minimax.io" # 海外版(如使用海外平台,请取消本行注释)
|
| 119 |
+
model: "MiniMax-M2.1"
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
**���始使用:**
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
mini-agent # 使用当前目录作为工作空间
|
| 126 |
+
mini-agent --workspace /path/to/your/project # 指定工作空间目录
|
| 127 |
+
mini-agent --version # 查看版本信息
|
| 128 |
+
|
| 129 |
+
# 管理命令
|
| 130 |
+
uv tool upgrade mini-agent # 升级到最新版本
|
| 131 |
+
uv tool uninstall mini-agent # 卸载工具(如需要)
|
| 132 |
+
uv tool list # 查看所有已安装的工具
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
#### 🔧 开发模式
|
| 136 |
+
|
| 137 |
+
此模式适合需要修改代码、添加功能或进行调试的开发者。
|
| 138 |
+
|
| 139 |
+
**安装与配置步骤:**
|
| 140 |
+
|
| 141 |
+
```bash
|
| 142 |
+
# 1. 克隆仓库
|
| 143 |
+
git clone https://github.com/MiniMax-AI/Mini-Agent.git
|
| 144 |
+
cd Mini-Agent
|
| 145 |
+
|
| 146 |
+
# 2. 安装 uv(如果尚未安装)
|
| 147 |
+
# macOS/Linux:
|
| 148 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 149 |
+
# Windows (PowerShell):
|
| 150 |
+
irm https://astral.sh/uv/install.ps1 | iex
|
| 151 |
+
# 安装后需要重启终端
|
| 152 |
+
|
| 153 |
+
# 3. 同步依赖
|
| 154 |
+
uv sync
|
| 155 |
+
|
| 156 |
+
# 替代方案: 手动安装依赖(如果不使用 uv)
|
| 157 |
+
# pip install -r requirements.txt
|
| 158 |
+
# 或者安装必需的包:
|
| 159 |
+
# pip install tiktoken pyyaml httpx pydantic requests prompt-toolkit mcp
|
| 160 |
+
|
| 161 |
+
# 4. 初始化 Claude Skills(可选)
|
| 162 |
+
git submodule update --init --recursive
|
| 163 |
+
|
| 164 |
+
# 5. 复制配置模板
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
**macOS/Linux:**
|
| 168 |
+
```bash
|
| 169 |
+
cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
**Windows:**
|
| 173 |
+
```powershell
|
| 174 |
+
Copy-Item mini_agent\config\config-example.yaml mini_agent\config\config.yaml
|
| 175 |
+
|
| 176 |
+
# 6. 编辑配置文件
|
| 177 |
+
vim mini_agent/config/config.yaml # 或使用您偏好的编辑器
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
填入您的 API Key 和对应的 API Base:
|
| 181 |
+
|
| 182 |
+
```yaml
|
| 183 |
+
api_key: "YOUR_API_KEY_HERE" # 填入第 1 步获取的 API Key
|
| 184 |
+
api_base: "https://api.minimaxi.com" # 国内版
|
| 185 |
+
# api_base: "https://api.minimax.io" # 海外版(如使用海外平台,请修改此行)
|
| 186 |
+
model: "MiniMax-M2.1"
|
| 187 |
+
max_steps: 100
|
| 188 |
+
workspace_dir: "./workspace"
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
> 📖 完整的配置指南,请参阅 [config-example.yaml](mini_agent/config/config-example.yaml)
|
| 192 |
+
|
| 193 |
+
**运行方式:**
|
| 194 |
+
|
| 195 |
+
选择您偏好的方式运行:
|
| 196 |
+
|
| 197 |
+
```bash
|
| 198 |
+
# 方式 1:作为模块直接运行(适合调试)
|
| 199 |
+
uv run python -m mini_agent.cli
|
| 200 |
+
|
| 201 |
+
# 方式 2:以可编辑模式安装(推荐)
|
| 202 |
+
uv tool install -e .
|
| 203 |
+
# 安装后,您可以在任何路径下运行,且代码更改会立即生效
|
| 204 |
+
mini-agent
|
| 205 |
+
mini-agent --workspace /path/to/your/project
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
> 📖 更多开发指引,请参阅 [开发指南](docs/DEVELOPMENT_GUIDE_CN.md)
|
| 209 |
+
|
| 210 |
+
> 📖 更多生产部署指引,请参阅 [生产指南](docs/PRODUCTION_GUIDE_CN.md)
|
| 211 |
+
|
| 212 |
+
## ACP & Zed Editor 集成(可选)
|
| 213 |
+
|
| 214 |
+
Mini Agent 支持 [Agent Communication Protocol (ACP)](https://github.com/modelcontextprotocol/protocol),可与 Zed 等代码编辑器集成。
|
| 215 |
+
|
| 216 |
+
**在 Zed Editor 中设置:**
|
| 217 |
+
|
| 218 |
+
1. 以开发模式或工具模式安装 Mini Agent
|
| 219 |
+
2. 在您的 Zed `settings.json` 中添加:
|
| 220 |
+
|
| 221 |
+
```json
|
| 222 |
+
{
|
| 223 |
+
"agent_servers": {
|
| 224 |
+
"mini-agent": {
|
| 225 |
+
"command": "/path/to/mini-agent-acp"
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
命令路径应为:
|
| 232 |
+
- 通过 `uv tool install` 安装:使用 `which mini-agent-acp` 的输出结果
|
| 233 |
+
- 开发模式:`./mini_agent/acp/server.py`
|
| 234 |
+
|
| 235 |
+
**使用方法:**
|
| 236 |
+
- 使用 `Ctrl+Shift+P` → "Agent: Toggle Panel" 打开 Zed 的 Agent 面板
|
| 237 |
+
- 从 Agent 下拉列表中选择 "mini-agent"
|
| 238 |
+
- 直接在编辑器中开始与 Mini Agent 对话
|
| 239 |
+
|
| 240 |
+
## 使用示例
|
| 241 |
+
|
| 242 |
+
这里有几个 Mini Agent 能力的演示。
|
| 243 |
+
|
| 244 |
+
### 任务执行
|
| 245 |
+
|
| 246 |
+
*在这个演示中,我们要求 Agent 创建一个简洁美观的网页并在浏览器中显示它,以此展示基础的工具使用循环。*
|
| 247 |
+
|
| 248 |
+

|
| 249 |
+
|
| 250 |
+
### 使用 Claude Skill(例如:PDF 生成)
|
| 251 |
+
|
| 252 |
+
*这里,Agent 利用 Claude Skill 根据用户请求创建专业文档(如 PDF 或 DOCX),展示了其强大的高级能力。*
|
| 253 |
+
|
| 254 |
+

|
| 255 |
+
|
| 256 |
+
### 网页搜索与摘要(MCP 工具)
|
| 257 |
+
|
| 258 |
+
*此演示展示了 Agent 如何使用其网页搜索工具在线查找最新信息,并为用户进行总结。*
|
| 259 |
+
|
| 260 |
+

|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
## 测试
|
| 264 |
+
|
| 265 |
+
项目包含了覆盖单元测试、功能测试和集成测试的全面测试用例。
|
| 266 |
+
|
| 267 |
+
### 快速运行
|
| 268 |
+
|
| 269 |
+
```bash
|
| 270 |
+
# 运行所有测试
|
| 271 |
+
pytest tests/ -v
|
| 272 |
+
|
| 273 |
+
# 仅运行核心功能测试
|
| 274 |
+
pytest tests/test_agent.py tests/test_note_tool.py -v
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
### 测试覆盖范围
|
| 278 |
+
|
| 279 |
+
- ✅ **单元测试** - 工具类、LLM 客户端
|
| 280 |
+
- ✅ **功能测试** - Session Note Tool、MCP 加载
|
| 281 |
+
- ✅ **集成测试** - Agent 端到端执行
|
| 282 |
+
- ✅ **外部服务** - Git MCP 服务器加载
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
## 常见问题
|
| 286 |
+
|
| 287 |
+
### SSL 证书错误
|
| 288 |
+
|
| 289 |
+
如果遇到 `[SSL: CERTIFICATE_VERIFY_FAILED]` 错误:
|
| 290 |
+
|
| 291 |
+
**测试环境快速修复** (修改 `mini_agent/llm.py`):
|
| 292 |
+
```python
|
| 293 |
+
# 第 50 行: 给 AsyncClient 添加 verify=False
|
| 294 |
+
async with httpx.AsyncClient(timeout=120.0, verify=False) as client:
|
| 295 |
+
```
|
| 296 |
+
|
| 297 |
+
**生产环境解决方案**:
|
| 298 |
+
```bash
|
| 299 |
+
# 更新证书
|
| 300 |
+
pip install --upgrade certifi
|
| 301 |
+
|
| 302 |
+
# 或配置系统代理/证书
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
### 模块未找到错误
|
| 306 |
+
|
| 307 |
+
确保从项目目录运行:
|
| 308 |
+
```bash
|
| 309 |
+
cd Mini-Agent
|
| 310 |
+
python -m mini_agent.cli
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
## 相关文档
|
| 314 |
+
|
| 315 |
+
- [开发指南](docs/DEVELOPMENT_GUIDE_CN.md) - 详细的开发和配置指引
|
| 316 |
+
- [生产环境指南](docs/PRODUCTION_DEPLOYMENT_GUIDE_CN.md) - 生产部署最佳实践
|
| 317 |
+
|
| 318 |
+
## 社区
|
| 319 |
+
|
| 320 |
+
加入 MiniMax 官方社区,获取帮助、分享想法、了解最新动态:
|
| 321 |
+
|
| 322 |
+
- **微信交流群**:扫描 [联系我们](https://platform.minimaxi.com/docs/faq/contact-us) 页面的二维码加入官方交流群
|
| 323 |
+
|
| 324 |
+
## 贡献
|
| 325 |
+
|
| 326 |
+
我们欢迎并鼓励您提交 Issue 和 Pull Request!
|
| 327 |
+
|
| 328 |
+
- [贡献指南](CONTRIBUTING.md) - 如何为项目做贡献
|
| 329 |
+
- [行为准则](CODE_OF_CONDUCT.md) - 社区行为准则
|
| 330 |
+
|
| 331 |
+
## 许可证
|
| 332 |
+
|
| 333 |
+
本项目采用 [MIT 许可证](LICENSE) 授权。
|
| 334 |
+
|
| 335 |
+
## 参考资源
|
| 336 |
+
|
| 337 |
+
- MiniMax API: https://platform.minimaxi.com/document
|
| 338 |
+
- MiniMax-M2: https://github.com/MiniMax-AI/MiniMax-M2
|
| 339 |
+
- Anthropic API: https://docs.anthropic.com/claude/reference
|
| 340 |
+
- Claude Skills: https://github.com/anthropics/skills
|
| 341 |
+
- MCP Servers: https://github.com/modelcontextprotocol/servers
|
| 342 |
+
|
| 343 |
+
---
|
| 344 |
+
|
| 345 |
+
**⭐ 如果这个项目对您有帮助,请给它一个 Star!**
|
docs/DEVELOPMENT_GUIDE.md
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Development Guide
|
| 2 |
+
|
| 3 |
+
## Table of Contents
|
| 4 |
+
|
| 5 |
+
- [Development Guide](#development-guide)
|
| 6 |
+
- [Table of Contents](#table-of-contents)
|
| 7 |
+
- [1. Project Architecture](#1-project-architecture)
|
| 8 |
+
- [2. Basic Usage](#2-basic-usage)
|
| 9 |
+
- [2.1 Interactive Commands](#21-interactive-commands)
|
| 10 |
+
- [2.2 Integrated MCP Tools](#22-integrated-mcp-tools)
|
| 11 |
+
- [Memory - Knowledge Graph Memory System](#memory---knowledge-graph-memory-system)
|
| 12 |
+
- [MiniMax Search - Web Search and Browse](#minimax-search---web-search-and-browse)
|
| 13 |
+
- [3. Extended Abilities](#3-extended-abilities)
|
| 14 |
+
- [3.1 Adding Custom Tools](#31-adding-custom-tools)
|
| 15 |
+
- [Steps](#steps)
|
| 16 |
+
- [Example](#example)
|
| 17 |
+
- [3.2 Adding MCP Tools](#32-adding-mcp-tools)
|
| 18 |
+
- [3.3 Customizing Note Storage](#33-customizing-note-storage)
|
| 19 |
+
- [3.4 Initialize Claude Skills (Recommended)](#34-initialize-claude-skills-recommended)
|
| 20 |
+
- [3.5 Adding a New Skill](#35-adding-a-new-skill)
|
| 21 |
+
- [3.6 Customizing System Prompt](#36-customizing-system-prompt)
|
| 22 |
+
- [What You Can Customize](#what-you-can-customize)
|
| 23 |
+
- [4. Troubleshooting](#4-troubleshooting)
|
| 24 |
+
- [4.1 Common Issues](#41-common-issues)
|
| 25 |
+
- [API Key Configuration Error](#api-key-configuration-error)
|
| 26 |
+
- [Dependency Installation Failure](#dependency-installation-failure)
|
| 27 |
+
- [MCP Tool Loading Failure](#mcp-tool-loading-failure)
|
| 28 |
+
- [4.2 Debugging Tips](#42-debugging-tips)
|
| 29 |
+
- [Enable Verbose Logging](#enable-verbose-logging)
|
| 30 |
+
- [Using the Python Debugger](#using-the-python-debugger)
|
| 31 |
+
- [Inspecting Tool Calls](#inspecting-tool-calls)
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## 1. Project Architecture
|
| 36 |
+
|
| 37 |
+
```
|
| 38 |
+
mini-agent/
|
| 39 |
+
├── mini_agent/ # Core source code
|
| 40 |
+
│ ├── agent.py # Main agent loop
|
| 41 |
+
│ ├── llm.py # LLM client
|
| 42 |
+
│ ├── cli.py # Command-line interface
|
| 43 |
+
│ ├── config.py # Configuration loading
|
| 44 |
+
│ ├── tools/ # Tool implementations (file, bash, MCP, skills, etc.)
|
| 45 |
+
│ └── skills/ # Claude Skills (submodule)
|
| 46 |
+
├── tests/ # Test code
|
| 47 |
+
├── docs/ # Documentation
|
| 48 |
+
├── workspace/ # Working directory
|
| 49 |
+
└── pyproject.toml # Project configuration
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## 2. Basic Usage
|
| 53 |
+
|
| 54 |
+
### 2.1 Interactive Commands
|
| 55 |
+
|
| 56 |
+
When running the agent in interactive mode (`mini-agent`), the following commands are available:
|
| 57 |
+
|
| 58 |
+
| Command | Description |
|
| 59 |
+
| ---------------------- | ----------------------------------------------------------- |
|
| 60 |
+
| `/exit`, `/quit`, `/q` | Exit the agent and display session statistics |
|
| 61 |
+
| `/help` | Display help information and available commands |
|
| 62 |
+
| `/clear` | Clear message history and start a new session |
|
| 63 |
+
| `/history` | Show the current session message count |
|
| 64 |
+
| `/stats` | Display session statistics (steps, tool calls, tokens used) |
|
| 65 |
+
|
| 66 |
+
### 2.2 Integrated MCP Tools
|
| 67 |
+
|
| 68 |
+
This project comes with pre-configured MCP (Model Context Protocol) tools that extend the agent's capabilities:
|
| 69 |
+
|
| 70 |
+
#### Memory - Knowledge Graph Memory System
|
| 71 |
+
|
| 72 |
+
**Function**: Provides long-term memory storage and retrieval based on graph database
|
| 73 |
+
|
| 74 |
+
**Status**: Enabled by default (`disabled: false`)
|
| 75 |
+
|
| 76 |
+
**Configuration**: No API Key required, works out of the box
|
| 77 |
+
|
| 78 |
+
**Capabilities**:
|
| 79 |
+
- Store and retrieve information across sessions
|
| 80 |
+
- Build knowledge graphs from conversations
|
| 81 |
+
- Semantic search through stored memories
|
| 82 |
+
|
| 83 |
+
---
|
| 84 |
+
|
| 85 |
+
#### MiniMax Search - Web Search and Browse
|
| 86 |
+
|
| 87 |
+
**Function**: Provides three powerful tools:
|
| 88 |
+
- `search` - Web search capability
|
| 89 |
+
- `parallel_search` - Execute multiple searches simultaneously
|
| 90 |
+
- `browse` - Intelligent web browsing and content extraction
|
| 91 |
+
|
| 92 |
+
**Status**: Disabled by default, needs configuration to enable
|
| 93 |
+
|
| 94 |
+
**Configuration Example**
|
| 95 |
+
|
| 96 |
+
```json
|
| 97 |
+
{
|
| 98 |
+
"mcpServers": {
|
| 99 |
+
"minimax_search": {
|
| 100 |
+
"disabled": false,
|
| 101 |
+
"env": {
|
| 102 |
+
"JINA_API_KEY": "your-jina-api-key",
|
| 103 |
+
"SERPER_API_KEY": "your-serper-api-key",
|
| 104 |
+
"MINIMAX_TOKEN": "your-minimax-token"
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
## 3. Extended Abilities
|
| 112 |
+
|
| 113 |
+
### 3.1 Adding Custom Tools
|
| 114 |
+
|
| 115 |
+
#### Steps
|
| 116 |
+
|
| 117 |
+
1. Create a new tool file under `mini_agent/tools/`.
|
| 118 |
+
2. Inherit from the `Tool` base class.
|
| 119 |
+
3. Implement the required properties and methods.
|
| 120 |
+
4. Register the tool during Agent initialization.
|
| 121 |
+
|
| 122 |
+
#### Example
|
| 123 |
+
|
| 124 |
+
```python
|
| 125 |
+
# mini_agent/tools/my_tool.py
|
| 126 |
+
from mini_agent.tools.base import Tool, ToolResult
|
| 127 |
+
from typing import Dict, Any
|
| 128 |
+
|
| 129 |
+
class MyTool(Tool):
|
| 130 |
+
@property
|
| 131 |
+
def name(self) -> str:
|
| 132 |
+
"""A unique name for the tool."""
|
| 133 |
+
return "my_tool"
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def description(self) -> str:
|
| 137 |
+
"""A description for the LLM to understand the tool's purpose."""
|
| 138 |
+
return "My custom tool for doing something useful"
|
| 139 |
+
|
| 140 |
+
@property
|
| 141 |
+
def parameters(self) -> Dict[str, Any]:
|
| 142 |
+
"""Parameter schema in JSON Schema format."""
|
| 143 |
+
return {
|
| 144 |
+
"type": "object",
|
| 145 |
+
"properties": {
|
| 146 |
+
"param1": {
|
| 147 |
+
"type": "string",
|
| 148 |
+
"description": "First parameter"
|
| 149 |
+
},
|
| 150 |
+
"param2": {
|
| 151 |
+
"type": "integer",
|
| 152 |
+
"description": "Second parameter",
|
| 153 |
+
"default": 10
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
"required": ["param1"]
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
async def execute(self, param1: str, param2: int = 10) -> ToolResult:
|
| 160 |
+
"""
|
| 161 |
+
The main logic of the tool.
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
param1: The first parameter.
|
| 165 |
+
param2: The second parameter, with a default value.
|
| 166 |
+
|
| 167 |
+
Returns:
|
| 168 |
+
A ToolResult object.
|
| 169 |
+
"""
|
| 170 |
+
try:
|
| 171 |
+
# Implement your logic here
|
| 172 |
+
result = f"Processed {param1} with param2={param2}"
|
| 173 |
+
|
| 174 |
+
return ToolResult(
|
| 175 |
+
success=True,
|
| 176 |
+
content=result
|
| 177 |
+
)
|
| 178 |
+
except Exception as e:
|
| 179 |
+
return ToolResult(
|
| 180 |
+
success=False,
|
| 181 |
+
content=f"Error: {str(e)}"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
# In cli.py or agent initialization code
|
| 185 |
+
from mini_agent.tools.my_tool import MyTool
|
| 186 |
+
|
| 187 |
+
# Add the new tool when creating the Agent
|
| 188 |
+
tools = [
|
| 189 |
+
ReadTool(workspace_dir),
|
| 190 |
+
WriteTool(workspace_dir),
|
| 191 |
+
MyTool(), # Add your custom tool
|
| 192 |
+
]
|
| 193 |
+
|
| 194 |
+
agent = Agent(
|
| 195 |
+
llm=llm,
|
| 196 |
+
tools=tools,
|
| 197 |
+
max_steps=50
|
| 198 |
+
)
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
### 3.2 Adding MCP Tools
|
| 202 |
+
|
| 203 |
+
Edit `mcp.json` to add a new MCP Server:
|
| 204 |
+
|
| 205 |
+
```json
|
| 206 |
+
{
|
| 207 |
+
"mcpServers": {
|
| 208 |
+
"my_custom_mcp": {
|
| 209 |
+
"description": "My custom MCP server",
|
| 210 |
+
"type": "stdio",
|
| 211 |
+
"command": "npx",
|
| 212 |
+
"args": ["-y", "@my-org/my-mcp-server"],
|
| 213 |
+
"env": {
|
| 214 |
+
"API_KEY": "your-api-key"
|
| 215 |
+
},
|
| 216 |
+
"disabled": false,
|
| 217 |
+
"notes": {
|
| 218 |
+
"description": "This is a custom MCP server.",
|
| 219 |
+
"api_key_url": "https://example.com/api-keys"
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
### 3.3 Customizing Note Storage
|
| 227 |
+
|
| 228 |
+
To replace the storage backend for the `SessionNoteTool`:
|
| 229 |
+
|
| 230 |
+
```python
|
| 231 |
+
# Current implementation: JSON file
|
| 232 |
+
class SessionNoteTool:
|
| 233 |
+
def __init__(self, memory_file: str = "./workspace/.agent_memory.json"):
|
| 234 |
+
self.memory_file = Path(memory_file)
|
| 235 |
+
|
| 236 |
+
async def _save_notes(self, notes: List[Dict]):
|
| 237 |
+
with open(self.memory_file, 'w') as f:
|
| 238 |
+
json.dump(notes, f, indent=2, ensure_ascii=False)
|
| 239 |
+
|
| 240 |
+
# Example extension: PostgreSQL
|
| 241 |
+
class PostgresNoteTool(Tool):
|
| 242 |
+
def __init__(self, db_url: str):
|
| 243 |
+
self.db = PostgresDB(db_url)
|
| 244 |
+
|
| 245 |
+
async def _save_notes(self, notes: List[Dict]):
|
| 246 |
+
await self.db.execute(
|
| 247 |
+
"INSERT INTO notes (content, category, timestamp) VALUES ($1, $2, $3)",
|
| 248 |
+
notes
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
# Example extension: Vector Database
|
| 252 |
+
class MilvusNoteTool(Tool):
|
| 253 |
+
def __init__(self, milvus_host: str):
|
| 254 |
+
self.vector_db = MilvusClient(host=milvus_host)
|
| 255 |
+
|
| 256 |
+
async def _save_notes(self, notes: List[Dict]):
|
| 257 |
+
# Generate embeddings
|
| 258 |
+
embeddings = await self.get_embeddings([n["content"] for n in notes])
|
| 259 |
+
|
| 260 |
+
# Store in the vector database
|
| 261 |
+
await self.vector_db.insert(
|
| 262 |
+
collection="agent_notes",
|
| 263 |
+
data=notes,
|
| 264 |
+
embeddings=embeddings
|
| 265 |
+
)
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
### 3.4 Initialize Claude Skills (Recommended)
|
| 269 |
+
|
| 270 |
+
This project integrates Claude's official skills repository via git submodule. Initialize it after first clone:
|
| 271 |
+
|
| 272 |
+
```bash
|
| 273 |
+
# Initialize submodule
|
| 274 |
+
git submodule update --init --recursive
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
Skills provide 20+ professional capabilities, making the Agent work like a professional:
|
| 278 |
+
|
| 279 |
+
- 📄 **Document Processing**: Create and edit PDF, DOCX, XLSX, PPTX
|
| 280 |
+
- 🎨 **Design Creation**: Generate artwork, posters, GIF animations
|
| 281 |
+
- 🧪 **Development & Testing**: Web automation testing (Playwright), MCP server development
|
| 282 |
+
- 🏢 **Enterprise Applications**: Internal communication, brand guidelines, theme customization
|
| 283 |
+
|
| 284 |
+
✨ **This is one of the core highlights of this project.** For details, see the "Configure Skills" section below.
|
| 285 |
+
|
| 286 |
+
**More information:**
|
| 287 |
+
|
| 288 |
+
- [Claude Skills Official Documentation](https://docs.claude.com/zh-CN/docs/agents-and-tools/agent-skills)
|
| 289 |
+
- [Anthropic Blog: Equipping agents for the real world](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)
|
| 290 |
+
|
| 291 |
+
### 3.5 Adding a New Skill
|
| 292 |
+
|
| 293 |
+
Create a custom Skill:
|
| 294 |
+
|
| 295 |
+
```bash
|
| 296 |
+
# Create a new skill directory under skills/
|
| 297 |
+
mkdir skills/my-custom-skill
|
| 298 |
+
cd skills/my-custom-skill
|
| 299 |
+
|
| 300 |
+
# Create the SKILL.md file
|
| 301 |
+
cat > SKILL.md << 'EOF'
|
| 302 |
+
---
|
| 303 |
+
name: my-custom-skill
|
| 304 |
+
description: My custom skill for handling specific tasks.
|
| 305 |
+
---
|
| 306 |
+
|
| 307 |
+
# Overview
|
| 308 |
+
|
| 309 |
+
This skill provides the following capabilities:
|
| 310 |
+
- Capability 1
|
| 311 |
+
- Capability 2
|
| 312 |
+
|
| 313 |
+
# Usage
|
| 314 |
+
|
| 315 |
+
1. Step one...
|
| 316 |
+
2. Step two...
|
| 317 |
+
|
| 318 |
+
# Best Practices
|
| 319 |
+
|
| 320 |
+
- Practice 1
|
| 321 |
+
- Practice 2
|
| 322 |
+
|
| 323 |
+
# FAQ
|
| 324 |
+
|
| 325 |
+
Q: Question 1
|
| 326 |
+
A: Answer 1
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
The new Skill will be automatically loaded and recognized by the Agent.
|
| 330 |
+
|
| 331 |
+
### 3.6 Customizing System Prompt
|
| 332 |
+
|
| 333 |
+
The system prompt (`system_prompt.md`) defines the Agent's behavior, capabilities, and working guidelines. You can customize it to tailor the Agent for specific use cases.
|
| 334 |
+
|
| 335 |
+
#### What You Can Customize
|
| 336 |
+
|
| 337 |
+
1. **Core Capabilities**: Add or modify tool descriptions
|
| 338 |
+
2. **Working Guidelines**: Define custom workflows and best practices
|
| 339 |
+
3. **Domain-Specific Knowledge**: Add expertise in specific areas
|
| 340 |
+
4. **Communication Style**: Adjust how the Agent interacts with users
|
| 341 |
+
5. **Task Priorities**: Set preferences for how tasks should be approached
|
| 342 |
+
|
| 343 |
+
After modifying `system_prompt.md`, be sure to restart the Agent to apply changes
|
| 344 |
+
|
| 345 |
+
## 4. Troubleshooting
|
| 346 |
+
|
| 347 |
+
### 4.1 Common Issues
|
| 348 |
+
|
| 349 |
+
#### API Key Configuration Error
|
| 350 |
+
|
| 351 |
+
```bash
|
| 352 |
+
# Error message
|
| 353 |
+
Error: Invalid API key
|
| 354 |
+
|
| 355 |
+
# Solution
|
| 356 |
+
1. Check that the API key in `config.yaml` is correct.
|
| 357 |
+
2. Ensure there are no extra spaces or quotes.
|
| 358 |
+
3. Verify that the API key has not expired.
|
| 359 |
+
```
|
| 360 |
+
|
| 361 |
+
#### Dependency Installation Failure
|
| 362 |
+
|
| 363 |
+
```bash
|
| 364 |
+
# Error message
|
| 365 |
+
uv sync failed
|
| 366 |
+
|
| 367 |
+
# Solution
|
| 368 |
+
1. Update uv to the latest version: `uv self update`
|
| 369 |
+
2. Clear the cache: `uv cache clean`
|
| 370 |
+
3. Try syncing again: `uv sync`
|
| 371 |
+
```
|
| 372 |
+
|
| 373 |
+
#### MCP Tool Loading Failure
|
| 374 |
+
|
| 375 |
+
```bash
|
| 376 |
+
# Error message
|
| 377 |
+
Failed to load MCP server
|
| 378 |
+
|
| 379 |
+
# Solution
|
| 380 |
+
1. Check the configuration in `mcp.json` is correct.
|
| 381 |
+
2. Ensure Node.js is installed (required for most MCP tools).
|
| 382 |
+
3. Verify that any required API keys are configured.
|
| 383 |
+
4. View detailed logs: `pytest tests/test_mcp.py -v -s`
|
| 384 |
+
```
|
| 385 |
+
|
| 386 |
+
### 4.2 Debugging Tips
|
| 387 |
+
|
| 388 |
+
#### Enable Verbose Logging
|
| 389 |
+
|
| 390 |
+
```python
|
| 391 |
+
# At the beginning of cli.py or a test file
|
| 392 |
+
import logging
|
| 393 |
+
|
| 394 |
+
logging.basicConfig(
|
| 395 |
+
level=logging.DEBUG,
|
| 396 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 397 |
+
)
|
| 398 |
+
```
|
| 399 |
+
|
| 400 |
+
#### Using the Python Debugger
|
| 401 |
+
|
| 402 |
+
```python
|
| 403 |
+
# Set a breakpoint in your code
|
| 404 |
+
import pdb; pdb.set_trace()
|
| 405 |
+
|
| 406 |
+
# Or use ipdb for a better experience
|
| 407 |
+
import ipdb; ipdb.set_trace()
|
| 408 |
+
```
|
| 409 |
+
|
| 410 |
+
#### Inspecting Tool Calls
|
| 411 |
+
|
| 412 |
+
```python
|
| 413 |
+
# Add logging in the Agent to see tool interactions
|
| 414 |
+
logger.debug(f"Tool call: {tool_call.name}")
|
| 415 |
+
logger.debug(f"Tool arguments: {tool_call.arguments}")
|
| 416 |
+
logger.debug(f"Tool result: {result.content[:200]}")
|
| 417 |
+
```
|
docs/DEVELOPMENT_GUIDE_CN.md
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 开发指南
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
## 目录
|
| 5 |
+
|
| 6 |
+
- [开发指南](#开发指南)
|
| 7 |
+
- [目录](#目录)
|
| 8 |
+
- [1. 项目架构](#1-项目架构)
|
| 9 |
+
- [2. 基础使用](#2-基础使用)
|
| 10 |
+
- [2.1 交互式命令](#21-交互式命令)
|
| 11 |
+
- [2.2 已集成的 MCP 工具](#22-已集成的-mcp-工具)
|
| 12 |
+
- [Memory - 知识图谱记忆系统](#memory---知识图谱记忆系统)
|
| 13 |
+
- [MiniMax Search - 网页搜索与浏览](#minimax-search---网页搜索与浏览)
|
| 14 |
+
- [3. 扩展能力](#3-扩展能力)
|
| 15 |
+
- [3.1 添加自定义工具](#31-添加自定义工具)
|
| 16 |
+
- [步骤](#步骤)
|
| 17 |
+
- [示例](#示例)
|
| 18 |
+
- [3.2 添加 MCP 工具](#32-添加-mcp-工具)
|
| 19 |
+
- [3.3 自定义存储](#33-自定义存储)
|
| 20 |
+
- [3.4 初始化 Claude Skills(推荐)](#34-初始化-claude-skills推荐)
|
| 21 |
+
- [3.5 添加新的Skill](#35-添加新的skill)
|
| 22 |
+
- [3.6 自定义系统提示词](#36-自定义系统提示词)
|
| 23 |
+
- [可定制内容包括:](#可定制内容包括)
|
| 24 |
+
- [4. 故障排查](#4-故障排查)
|
| 25 |
+
- [4.1 常见问题](#41-常见问题)
|
| 26 |
+
- [API 密钥配置错误](#api-密钥配置错误)
|
| 27 |
+
- [依赖安装失败](#依赖安装失败)
|
| 28 |
+
- [MCP 工具加载失败](#mcp-工具加载失败)
|
| 29 |
+
- [4.2 调试技巧](#42-调试技巧)
|
| 30 |
+
- [启用 Debug 日志](#启用-debug-日志)
|
| 31 |
+
- [使用 Python 调试器](#使用-python-调试器)
|
| 32 |
+
- [监控工具调用](#监控工具调用)
|
| 33 |
+
|
| 34 |
+
---
|
| 35 |
+
|
| 36 |
+
## 1. 项目架构
|
| 37 |
+
|
| 38 |
+
```
|
| 39 |
+
mini-agent/
|
| 40 |
+
├── mini_agent/ # 核心源代码
|
| 41 |
+
│ ├── agent.py # 主 Agent 循环
|
| 42 |
+
│ ├── llm.py # LLM 客户端
|
| 43 |
+
│ ├── cli.py # 命令行接口
|
| 44 |
+
│ ├── config.py # 配置加载
|
| 45 |
+
│ ├── tools/ # 工具实现(文件、Bash、MCP、技能等)
|
| 46 |
+
│ └── skills/ # Claude 技能集(子模块)
|
| 47 |
+
├── tests/ # 测试代码
|
| 48 |
+
├── docs/ # 文档
|
| 49 |
+
├── workspace/ # 工作目录
|
| 50 |
+
└── pyproject.toml # 项目配置
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## 2. 基础使用
|
| 54 |
+
|
| 55 |
+
### 2.1 交互式命令
|
| 56 |
+
|
| 57 |
+
在交互模式 (通过 `mini-agent` 启动) 下运行 Agent 时,您可以使用以下命令:
|
| 58 |
+
|
| 59 |
+
| 命令 | 说明 |
|
| 60 |
+
| ---------------------- | ------------------------------------------------ |
|
| 61 |
+
| `/exit`, `/quit`, `/q` | 退出 Agent 并显示会话统计信息 |
|
| 62 |
+
| `/help` | 显示帮助信息和可用命令 |
|
| 63 |
+
| `/clear` | 清除消息历史并开始新会话 |
|
| 64 |
+
| `/history` | 显示当前会话的消息数量 |
|
| 65 |
+
| `/stats` | 显示会话统计信息(步数、工具调用、使用的 Token) |
|
| 66 |
+
|
| 67 |
+
### 2.2 已集成的 MCP 工具
|
| 68 |
+
|
| 69 |
+
本项目预先集成了以下 MCP (模型上下文协议) 工具,用以扩展 Agent 的能力:
|
| 70 |
+
|
| 71 |
+
#### Memory - 知识图谱记忆系统
|
| 72 |
+
|
| 73 |
+
**功能**:基于图数据库,为 Agent 提供长期记忆的存储与检索能力。
|
| 74 |
+
|
| 75 |
+
**状态**:默认启用
|
| 76 |
+
|
| 77 |
+
**配置**:无需 API Key,开箱即用
|
| 78 |
+
|
| 79 |
+
**能力**:
|
| 80 |
+
- 跨会话存储并检索信息
|
| 81 |
+
- 根据对话内容构建知识图谱
|
| 82 |
+
- 对已存储的记忆进行语义搜索
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
#### MiniMax Search - 网页搜索与浏览
|
| 87 |
+
|
| 88 |
+
**功能**:提供三大强大工具:
|
| 89 |
+
- `search` - 网页搜索
|
| 90 |
+
- `parallel_search` - 并行执行多个搜索任务
|
| 91 |
+
- `browse` - 智能网页浏览与内容提取
|
| 92 |
+
|
| 93 |
+
**状态**:默认禁用,需要配置 API Key 后方可启用。
|
| 94 |
+
|
| 95 |
+
**配置示例**:
|
| 96 |
+
|
| 97 |
+
```json
|
| 98 |
+
{
|
| 99 |
+
"mcpServers": {
|
| 100 |
+
"minimax_search": {
|
| 101 |
+
"disabled": false,
|
| 102 |
+
"env": {
|
| 103 |
+
"JINA_API_KEY": "your-jina-api-key",
|
| 104 |
+
"SERPER_API_KEY": "your-serper-api-key",
|
| 105 |
+
"MINIMAX_API_KEY": "your-minimax-token"
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
## 3. 扩展能力
|
| 113 |
+
|
| 114 |
+
### 3.1 添加自定义工具
|
| 115 |
+
|
| 116 |
+
#### 步骤
|
| 117 |
+
|
| 118 |
+
1. 在 `mini_agent/tools/` 目录下创建一个新的 Python 文件。
|
| 119 |
+
2. 在文件中定义一个新类,并继承 `Tool` 基类。
|
| 120 |
+
3. 在类中实现所需的属性和方法。
|
| 121 |
+
4. 在 Agent 初始化时注册你的新工具。
|
| 122 |
+
|
| 123 |
+
#### 示例
|
| 124 |
+
|
| 125 |
+
```python
|
| 126 |
+
# mini_agent/tools/my_tool.py
|
| 127 |
+
from mini_agent.tools.base import Tool, ToolResult
|
| 128 |
+
from typing import Dict, Any
|
| 129 |
+
|
| 130 |
+
class MyTool(Tool):
|
| 131 |
+
@property
|
| 132 |
+
def name(self) -> str:
|
| 133 |
+
"""工具的唯一名称,需保持独一无二。"""
|
| 134 |
+
return "my_tool"
|
| 135 |
+
|
| 136 |
+
@property
|
| 137 |
+
def description(self) -> str:
|
| 138 |
+
"""工具用途的详细描述,帮助 LLM 理解其功能。"""
|
| 139 |
+
return "我的自定义工具,用于完成特定任务"
|
| 140 |
+
|
| 141 |
+
@property
|
| 142 |
+
def parameters(self) -> Dict[str, Any]:
|
| 143 |
+
"""参数模式(JSON Schema 格式)。"""
|
| 144 |
+
return {
|
| 145 |
+
"type": "object",
|
| 146 |
+
"properties": {
|
| 147 |
+
"param1": {
|
| 148 |
+
"type": "string",
|
| 149 |
+
"description": "第一���参数"
|
| 150 |
+
},
|
| 151 |
+
"param2": {
|
| 152 |
+
"type": "integer",
|
| 153 |
+
"description": "第二个参数",
|
| 154 |
+
"default": 10
|
| 155 |
+
}
|
| 156 |
+
},
|
| 157 |
+
"required": ["param1"]
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
async def execute(self, param1: str, param2: int = 10) -> ToolResult:
|
| 161 |
+
"""
|
| 162 |
+
工具执行的核心逻辑。
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
param1: 参数一。
|
| 166 |
+
param2: 参数二,包含默认值。
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
返回一个 ToolResult 对象。
|
| 170 |
+
"""
|
| 171 |
+
try:
|
| 172 |
+
# 在此实现你的逻辑
|
| 173 |
+
result = f"处理了 {param1},param2={param2}"
|
| 174 |
+
|
| 175 |
+
return ToolResult(
|
| 176 |
+
success=True,
|
| 177 |
+
content=result
|
| 178 |
+
)
|
| 179 |
+
except Exception as e:
|
| 180 |
+
return ToolResult(
|
| 181 |
+
success=False,
|
| 182 |
+
content=f"错误: {str(e)}"
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# 在 cli.py 或 Agent 的初始化代码中
|
| 186 |
+
from mini_agent.tools.my_tool import MyTool
|
| 187 |
+
|
| 188 |
+
# 创建 Agent 实例时,将新工具加入列表
|
| 189 |
+
tools = [
|
| 190 |
+
ReadTool(workspace_dir),
|
| 191 |
+
WriteTool(workspace_dir),
|
| 192 |
+
MyTool(), # 添加您的自定义工具
|
| 193 |
+
]
|
| 194 |
+
|
| 195 |
+
agent = Agent(
|
| 196 |
+
llm=llm,
|
| 197 |
+
tools=tools,
|
| 198 |
+
max_steps=50
|
| 199 |
+
)
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
### 3.2 添加 MCP 工具
|
| 203 |
+
|
| 204 |
+
编辑 `mcp.json` 文件,即可添加新的 MCP 服务器:
|
| 205 |
+
|
| 206 |
+
```json
|
| 207 |
+
{
|
| 208 |
+
"mcpServers": {
|
| 209 |
+
"my_custom_mcp": {
|
| 210 |
+
"description": "我的自定义 MCP 服务器",
|
| 211 |
+
"type": "stdio",
|
| 212 |
+
"command": "npx",
|
| 213 |
+
"args": ["-y", "@my-org/my-mcp-server"],
|
| 214 |
+
"env": {
|
| 215 |
+
"API_KEY": "your-api-key"
|
| 216 |
+
},
|
| 217 |
+
"disabled": false,
|
| 218 |
+
"notes": {
|
| 219 |
+
"description": "这是一个自定义 MCP 服务器。",
|
| 220 |
+
"api_key_url": "https://example.com/api-keys"
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
### 3.3 自定义存储
|
| 228 |
+
|
| 229 |
+
您可以替换 `SessionNoteTool` 的默认存储实现,以对接不同的数据后端:
|
| 230 |
+
|
| 231 |
+
```python
|
| 232 |
+
# 默认实现:JSON 文件
|
| 233 |
+
class SessionNoteTool:
|
| 234 |
+
def __init__(self, memory_file: str = "./workspace/.agent_memory.json"):
|
| 235 |
+
self.memory_file = Path(memory_file)
|
| 236 |
+
|
| 237 |
+
async def _save_notes(self, notes: List[Dict]):
|
| 238 |
+
with open(self.memory_file, 'w') as f:
|
| 239 |
+
json.dump(notes, f, indent=2, ensure_ascii=False)
|
| 240 |
+
|
| 241 |
+
# 扩展示例:使用 PostgreSQL 存储
|
| 242 |
+
class PostgresNoteTool(Tool):
|
| 243 |
+
def __init__(self, db_url: str):
|
| 244 |
+
self.db = PostgresDB(db_url)
|
| 245 |
+
|
| 246 |
+
async def _save_notes(self, notes: List[Dict]):
|
| 247 |
+
await self.db.execute(
|
| 248 |
+
"INSERT INTO notes (content, category, timestamp) VALUES ($1, $2, $3)",
|
| 249 |
+
notes
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# 扩展示例:使用向量数据库存储
|
| 253 |
+
class MilvusNoteTool(Tool):
|
| 254 |
+
def __init__(self, milvus_host: str):
|
| 255 |
+
self.vector_db = MilvusClient(host=milvus_host)
|
| 256 |
+
|
| 257 |
+
async def _save_notes(self, notes: List[Dict]):
|
| 258 |
+
# 生成内容的嵌入向量
|
| 259 |
+
embeddings = await self.get_embeddings([n["content"] for n in notes])
|
| 260 |
+
|
| 261 |
+
# 将笔记和向量存入向量数据库
|
| 262 |
+
await self.vector_db.insert(
|
| 263 |
+
collection="agent_notes",
|
| 264 |
+
data=notes,
|
| 265 |
+
embeddings=embeddings
|
| 266 |
+
)
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
### 3.4 初始化 Claude Skills(推荐)
|
| 270 |
+
|
| 271 |
+
本项目通过 Git Submodule 的方式集成了 Claude 官方技能库。首次克隆项目后,请执行以下命令来初始化技能库:
|
| 272 |
+
|
| 273 |
+
```bash
|
| 274 |
+
# 初始化并拉取技能库子模块
|
| 275 |
+
git submodule update --init --recursive
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
Skills 库提供了超过20种专业能力,能让 Agent 如同行业专家般处理复杂任务:
|
| 279 |
+
|
| 280 |
+
- 📄 **文档处理**:轻松创建和编辑 PDF、DOCX、XLSX、PPTX 等格式的文档。
|
| 281 |
+
- 🎨 **设计创作**:生成富有创意的艺术作品、海报和 GIF 动画。
|
| 282 |
+
- 🧪 **开发与测试**:支持 Web 自动化测试 (Playwright) 和 MCP 服务器开发。
|
| 283 |
+
- 🏢 **企业应用**:高效处理内部沟通、品牌指南应用和主题定制等任务。
|
| 284 |
+
|
| 285 |
+
✨ **这是本项目的核心亮点之一。**
|
| 286 |
+
|
| 287 |
+
**更多信息:**
|
| 288 |
+
|
| 289 |
+
- [Claude Skills 官方文档](https://docs.claude.com/zh-CN/docs/agents-and-tools/agent-skills)
|
| 290 |
+
- [Anthropic 博客:为真实世界装备智能体](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)
|
| 291 |
+
|
| 292 |
+
### 3.5 添加新的Skill
|
| 293 |
+
|
| 294 |
+
您可以按照以下步骤创建自定义 Skill:
|
| 295 |
+
|
| 296 |
+
```bash
|
| 297 |
+
# 在 skills/ 目录下为您的新技能创建一个目录
|
| 298 |
+
mkdir skills/my-custom-skill
|
| 299 |
+
cd skills/my-custom-skill
|
| 300 |
+
|
| 301 |
+
# 创建技能描述文件 SKILL.md
|
| 302 |
+
cat > SKILL.md << 'EOF'
|
| 303 |
+
---
|
| 304 |
+
name: my-custom-skill
|
| 305 |
+
description: 这是一个自定义技能,用于处理特定任务。
|
| 306 |
+
---
|
| 307 |
+
|
| 308 |
+
# 概述
|
| 309 |
+
|
| 310 |
+
该技能主要提供以下功能:
|
| 311 |
+
- 功能 1
|
| 312 |
+
- 功能 2
|
| 313 |
+
|
| 314 |
+
# 使用方法
|
| 315 |
+
|
| 316 |
+
1. 第一步...
|
| 317 |
+
2. 第二步...
|
| 318 |
+
|
| 319 |
+
# 最佳实践
|
| 320 |
+
|
| 321 |
+
- 实践 1
|
| 322 |
+
- 实践 2
|
| 323 |
+
|
| 324 |
+
# 常见问题
|
| 325 |
+
|
| 326 |
+
问:问题 1
|
| 327 |
+
答:答案 1
|
| 328 |
+
EOF
|
| 329 |
+
```
|
| 330 |
+
|
| 331 |
+
完成以上步骤后,Agent 将在下次启动时自动加载并识别这项新技能。
|
| 332 |
+
|
| 333 |
+
### 3.6 自定义系统提示词
|
| 334 |
+
|
| 335 |
+
系统提示词文件 (`system_prompt.md`) 定义了 Agent 的核心行为、能力边界和工作指南。您可以根据具体应用场景,对其进行深度定制。
|
| 336 |
+
|
| 337 |
+
#### 可定制内容包括:
|
| 338 |
+
|
| 339 |
+
1. **核心能力**:添加或修改工具的描述,以影响 Agent 的工具选择。
|
| 340 |
+
2. **工作指南**:定义特定的工作流程或决策偏好。
|
| 341 |
+
3. **领域专业知识**:注入特定领域的知识,提升 Agent 的专业性。
|
| 342 |
+
4. **沟通风格**:调整 Agent 与用户交互时的语气和风格。
|
| 343 |
+
5. **任务优先级**:设定处理任务时的优先级和策略。
|
| 344 |
+
|
| 345 |
+
完成修改后,请重启 Agent 以使新配置生效。
|
| 346 |
+
|
| 347 |
+
## 4. 故障排查
|
| 348 |
+
|
| 349 |
+
### 4.1 常见问题
|
| 350 |
+
|
| 351 |
+
#### API 密钥配置错误
|
| 352 |
+
|
| 353 |
+
```bash
|
| 354 |
+
# 错误消息
|
| 355 |
+
Error: Invalid API key
|
| 356 |
+
|
| 357 |
+
# 解决方法
|
| 358 |
+
1. 检查 `config.yaml` 文件中的 API 密钥是否填写正确。
|
| 359 |
+
2. 确保密钥前后没有多余的空格或引号。
|
| 360 |
+
3. 确认该 API 密钥是否仍在有效期内。
|
| 361 |
+
```
|
| 362 |
+
|
| 363 |
+
#### 依赖安装失败
|
| 364 |
+
|
| 365 |
+
```bash
|
| 366 |
+
# 错误消息
|
| 367 |
+
uv sync failed
|
| 368 |
+
|
| 369 |
+
# 解决方法
|
| 370 |
+
1. 升级 uv 至最新版本:`uv self update`
|
| 371 |
+
2. 清理 uv 缓存:`uv cache clean`
|
| 372 |
+
3. 再次尝试同步依赖:`uv sync`
|
| 373 |
+
```
|
| 374 |
+
|
| 375 |
+
#### MCP 工具加载失败
|
| 376 |
+
|
| 377 |
+
```bash
|
| 378 |
+
# 错误消息
|
| 379 |
+
Failed to load MCP server
|
| 380 |
+
|
| 381 |
+
# 解决方法
|
| 382 |
+
1. 检查 `mcp.json` 文件中的服务器配置是否正确。
|
| 383 |
+
2. 确保您的开发环境已安装 Node.js (大部分 MCP 工具的运行需要)。
|
| 384 |
+
3. 确认所需服务的 API 密钥已正确配置。
|
| 385 |
+
4. 运行 MCP 测试并查看详细日志:`pytest tests/test_mcp.py -v -s`
|
| 386 |
+
```
|
| 387 |
+
|
| 388 |
+
### 4.2 调试技巧
|
| 389 |
+
|
| 390 |
+
#### 启用 Debug 日志
|
| 391 |
+
|
| 392 |
+
```python
|
| 393 |
+
# 在 cli.py 或相关测试文件的开头添加以下代码:
|
| 394 |
+
import logging
|
| 395 |
+
|
| 396 |
+
logging.basicConfig(
|
| 397 |
+
level=logging.DEBUG,
|
| 398 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 399 |
+
)
|
| 400 |
+
```
|
| 401 |
+
|
| 402 |
+
#### 使用 Python 调试器
|
| 403 |
+
|
| 404 |
+
```python
|
| 405 |
+
# 在需要暂停执行的代码行处插入断点:
|
| 406 |
+
import pdb; pdb.set_trace()
|
| 407 |
+
|
| 408 |
+
# 或者使用 ipdb 以获得更佳的调试体验:
|
| 409 |
+
import ipdb; ipdb.set_trace()
|
| 410 |
+
```
|
| 411 |
+
|
| 412 |
+
#### 监控工具调用
|
| 413 |
+
|
| 414 |
+
```python
|
| 415 |
+
# 在 Agent 代码中添加日志,以便实时查看工具的调用详情:
|
| 416 |
+
logger.debug(f"工具调用: {tool_call.name}")
|
| 417 |
+
logger.debug(f"工具参数: {tool_call.arguments}")
|
| 418 |
+
logger.debug(f"工具结果: {result.content[:200]}")
|
| 419 |
+
```
|
| 420 |
+
|
docs/PRODUCTION_GUIDE.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent Production Guide
|
| 2 |
+
|
| 3 |
+
> A Complete Guide from Demo to Production
|
| 4 |
+
|
| 5 |
+
## Table of Contents
|
| 6 |
+
|
| 7 |
+
- [1. Demo Features](#1-demo-features)
|
| 8 |
+
- [2. Upgrade Directions](#2-upgrade-directions)
|
| 9 |
+
- [3. Production Deployment](#3-production-deployment)
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## 1. Demo Features
|
| 14 |
+
|
| 15 |
+
This project is a **teaching-level demo** that demonstrates the core concepts and execution flow of an Agent. To reach production level, many complex issues still need to be addressed.
|
| 16 |
+
|
| 17 |
+
### What We've Implemented (Demo Level)
|
| 18 |
+
|
| 19 |
+
| Feature | Demo Implementation |
|
| 20 |
+
| --------------------- | --------------------------- |
|
| 21 |
+
| **Context Management** | ✅ Simple persistence via SessionNoteTool with file storage; basic summarization when approaching context window limit |
|
| 22 |
+
| **Tool Calling** | ✅ Basic Read/Write/Edit/Bash |
|
| 23 |
+
| **Error Handling** | ✅ Basic exception catching |
|
| 24 |
+
| **Logging** | ✅ Simple print output |
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
## 2. Upgrade Directions
|
| 28 |
+
|
| 29 |
+
### 2.1 Advanced Context Management
|
| 30 |
+
|
| 31 |
+
- Introduce distributed file systems for unified context persistence management and backup
|
| 32 |
+
- Use more precise methods for token counting
|
| 33 |
+
- Introduce more strategies for message compression, including keeping the most recent N messages, preserving fixed metadata, prompt optimization for summarization, introducing recall systems, etc.
|
| 34 |
+
|
| 35 |
+
### 2.2 Model Fallback Mechanism
|
| 36 |
+
|
| 37 |
+
Currently using a single fixed model (MiniMax-M2.1), which will directly report errors on failure.
|
| 38 |
+
|
| 39 |
+
- Introduce a model pool by configuring multiple model accounts to improve availability
|
| 40 |
+
- Introduce automatic health checks, failure removal, circuit breaker strategies for the model pool
|
| 41 |
+
|
| 42 |
+
### 2.3 Model Hallucination Detection and Correction
|
| 43 |
+
|
| 44 |
+
Currently directly trusts model output without validation mechanism
|
| 45 |
+
|
| 46 |
+
- Perform security checks on input parameters for certain tool calls to prevent high-risk actions
|
| 47 |
+
- Perform reflection on results from certain tool calls to check if they are reasonable
|
| 48 |
+
|
| 49 |
+
## 3. Production Deployment
|
| 50 |
+
|
| 51 |
+
### 3.1 Container Deployment Recommendations
|
| 52 |
+
|
| 53 |
+
We recommend using K8s/Docker environments for Agent deployment. Containerized deployment has the following advantages:
|
| 54 |
+
|
| 55 |
+
- **Resource Isolation**: Each Agent instance runs in an independent container without interference
|
| 56 |
+
- **Elastic Scaling**: Automatically adjust instance count based on load
|
| 57 |
+
- **Version Management**: Easy rollback and canary releases
|
| 58 |
+
- **Environment Consistency**: Development, testing, and production environments are completely consistent
|
| 59 |
+
|
| 60 |
+
### 3.2 Resource Limit Configuration
|
| 61 |
+
|
| 62 |
+
#### 3.2.1 CPU and Memory Limits
|
| 63 |
+
|
| 64 |
+
To prevent the Agent from consuming excessive CPU/Memory resources and affecting the host, CPU and memory limits must be set:
|
| 65 |
+
|
| 66 |
+
**Docker Configuration Example**:
|
| 67 |
+
```yaml
|
| 68 |
+
# docker-compose.yml
|
| 69 |
+
services:
|
| 70 |
+
agent:
|
| 71 |
+
image: agent-demo:latest
|
| 72 |
+
deploy:
|
| 73 |
+
resources:
|
| 74 |
+
limits:
|
| 75 |
+
cpus: '2.0' # Maximum 2 CPU cores
|
| 76 |
+
memory: 2G # Maximum 2GB memory
|
| 77 |
+
reservations:
|
| 78 |
+
cpus: '0.5' # Guarantee at least 0.5 cores
|
| 79 |
+
memory: 512M # Guarantee at least 512MB
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
#### 3.2.2 Disk Limits
|
| 83 |
+
|
| 84 |
+
Agents may generate large amounts of temporary files and log files, so disk usage needs to be limited:
|
| 85 |
+
|
| 86 |
+
**Docker Volume Configuration**:
|
| 87 |
+
```yaml
|
| 88 |
+
# docker-compose.yml
|
| 89 |
+
services:
|
| 90 |
+
agent:
|
| 91 |
+
volumes:
|
| 92 |
+
- type: tmpfs
|
| 93 |
+
target: /tmp
|
| 94 |
+
tmpfs:
|
| 95 |
+
size: 1G # Maximum 1GB for temporary files
|
| 96 |
+
- type: volume
|
| 97 |
+
source: agent-data
|
| 98 |
+
target: /app/data
|
| 99 |
+
volume:
|
| 100 |
+
driver_opts:
|
| 101 |
+
size: 5G # Maximum 5GB for data volume
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
### 3.3 Linux Account Permission Restrictions
|
| 106 |
+
|
| 107 |
+
#### 3.3.1 Principle of Least Privilege
|
| 108 |
+
|
| 109 |
+
**Never run the Agent as root user**, as this poses serious security risks.
|
| 110 |
+
|
| 111 |
+
**Dockerfile Best Practices**:
|
| 112 |
+
```dockerfile
|
| 113 |
+
FROM python:3.11-slim
|
| 114 |
+
|
| 115 |
+
# Install necessary system tools
|
| 116 |
+
RUN apt-get update && apt-get install -y \
|
| 117 |
+
git \
|
| 118 |
+
curl \
|
| 119 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 120 |
+
|
| 121 |
+
# Install uv
|
| 122 |
+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 123 |
+
ENV PATH="/root/.cargo/bin:$PATH"
|
| 124 |
+
|
| 125 |
+
# Create non-privileged user
|
| 126 |
+
RUN groupadd -r agent && useradd -r -g agent agent
|
| 127 |
+
|
| 128 |
+
# Set working directory
|
| 129 |
+
WORKDIR /app
|
| 130 |
+
|
| 131 |
+
# Option 1: Clone from Git repository (for public repos)
|
| 132 |
+
RUN git clone https://github.com/MiniMax-AI/agent-demo.git . && \
|
| 133 |
+
chown -R agent:agent /app
|
| 134 |
+
|
| 135 |
+
# Option 2: Copy code from local (for private deployments)
|
| 136 |
+
# COPY --chown=agent:agent . /app
|
| 137 |
+
|
| 138 |
+
# Switch to non-privileged user before installing dependencies
|
| 139 |
+
USER agent
|
| 140 |
+
|
| 141 |
+
# Sync dependencies using uv
|
| 142 |
+
RUN uv sync
|
| 143 |
+
|
| 144 |
+
# Start the application
|
| 145 |
+
CMD ["uv", "run", "mini-agent"]
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
#### 3.3.2 File System Permissions
|
| 149 |
+
|
| 150 |
+
Restrict the Agent to only access necessary directories:
|
| 151 |
+
|
| 152 |
+
```bash
|
| 153 |
+
# Create restricted workspace directory
|
| 154 |
+
mkdir -p /app/workspace
|
| 155 |
+
chown agent:agent /app/workspace
|
| 156 |
+
chmod 750 /app/workspace # Owner: read/write/execute, Group: read/execute
|
| 157 |
+
|
| 158 |
+
# Restrict access to sensitive directories
|
| 159 |
+
chmod 700 /etc/agent # Config directory only accessible by owner
|
| 160 |
+
chmod 600 /etc/agent/*.yaml # Config files only readable/writable by owner
|
| 161 |
+
```
|
| 162 |
+
|
docs/PRODUCTION_GUIDE_CN.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent 生产环境指南
|
| 2 |
+
|
| 3 |
+
> 从 Demo 到生产环境的实践指南
|
| 4 |
+
|
| 5 |
+
## 目录
|
| 6 |
+
|
| 7 |
+
- [1. Demo功能实现](#1-demo功能)
|
| 8 |
+
- [2. 可升级方向](#2-可升级方向)
|
| 9 |
+
- [3. 生产部署](#3-生产部署)
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## 1. Demo 功能概述
|
| 14 |
+
|
| 15 |
+
本项目是一个**教学级 Demo**,旨在演示 Agent 的核心概念与基本执行流程。要在生产环境中使用,还需要解决一系列复杂问题。
|
| 16 |
+
|
| 17 |
+
### Demo 实现的功能
|
| 18 |
+
|
| 19 |
+
| 功能 | Demo 实现 |
|
| 20 |
+
| --------------------- | --------------------------- |
|
| 21 |
+
| **上下文管理** | ✅ 通过 `SessionNoteTool` 以文件形式实现了简单的上下文持久化;当接近上下文窗口上限时,会进行简单的摘要处理。 |
|
| 22 |
+
| **工具调用** | ✅ 提供了基础的 Read/Write/Edit/Bash 工具。 |
|
| 23 |
+
| **错误处理** | ✅ 实现了基础的异常捕获机制。 |
|
| 24 |
+
| **日志** | ✅ 使用简单的 `print` 函数输出日志。 |
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
## 2. 升级与拓展方向
|
| 28 |
+
|
| 29 |
+
### 2.1 高级上下文管理
|
| 30 |
+
|
| 31 |
+
- **引入分布式文件系统**:对上下文进行统一的持久化管理和备份。
|
| 32 |
+
- **优化 Token 计算**:使用更精确的方式计算 Token 数量。
|
| 33 |
+
- **丰富消息压缩策略**:引入更丰富的消息压缩策略,例如保留最近 N 条消息、保留核心元信息、优化摘要 Prompt,或集成召回系统等。
|
| 34 |
+
|
| 35 |
+
### 2.2 模型回退机制
|
| 36 |
+
|
| 37 |
+
当前 Demo 固定使用单一模型(MiniMax-M2.1),调用失败时会直接报错。
|
| 38 |
+
|
| 39 |
+
- **建立模型池**:配置多个模型账号,建立模型池以提高服务可用性。
|
| 40 |
+
- **引入高可用策略**:为模型池引入自动健康检测、故障节点切换、熔断等高可用策略。
|
| 41 |
+
|
| 42 |
+
### 2.3 模型幻觉的检测与修正
|
| 43 |
+
|
| 44 |
+
当前 Demo 完全信任模型输出,缺乏验证机制。
|
| 45 |
+
|
| 46 |
+
- **输入参数安全检查**:对部分工具的调用参数进行安全性检查,防止执行高危操作。
|
| 47 |
+
- **输出结果合理性检查**:对部分工具的调用结果进行反思(Self-reflection),检查其合理性。
|
| 48 |
+
|
| 49 |
+
## 3. 生产环境部署
|
| 50 |
+
|
| 51 |
+
### 3.1 容器化部署建议
|
| 52 |
+
|
| 53 |
+
我们推荐使用 Kubernetes 或 Docker 环境来部署 Agent。容器化部署具有以下优势:
|
| 54 |
+
|
| 55 |
+
- **资源隔离**:每个 Agent 实例运行在独立的容器中,互不干扰。
|
| 56 |
+
- **弹性扩展**:根据负载自动调整实例数量。
|
| 57 |
+
- **版本管理**:便于快速回滚和灰度发布。
|
| 58 |
+
- **环境一致性**:开发、测试、生产环境完全一致。
|
| 59 |
+
|
| 60 |
+
### 3.2 资源限制
|
| 61 |
+
|
| 62 |
+
#### 3.2.1 CPU 与内存限制
|
| 63 |
+
|
| 64 |
+
为防止 Agent 实例占用过多资源而影响宿主机,您必须为其设置 CPU 和内存的限制:
|
| 65 |
+
|
| 66 |
+
**Docker 配置示例**:
|
| 67 |
+
```yaml
|
| 68 |
+
# docker-compose.yml
|
| 69 |
+
services:
|
| 70 |
+
agent:
|
| 71 |
+
image: agent-demo:latest
|
| 72 |
+
deploy:
|
| 73 |
+
resources:
|
| 74 |
+
limits:
|
| 75 |
+
cpus: '2.0' # 最多使用 2 个 CPU 核心
|
| 76 |
+
memory: 2G # 最多使用 2GB 内存
|
| 77 |
+
reservations:
|
| 78 |
+
cpus: '0.5' # 保证至少 0.5 个核心
|
| 79 |
+
memory: 512M # 保证至少 512MB
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
#### 3.2.2 磁盘限制
|
| 83 |
+
|
| 84 |
+
Agent 运行过程中可能会产生大量的临时文件和日志,因此需要限制其磁盘使用量:
|
| 85 |
+
|
| 86 |
+
**Docker Volume 配置**:
|
| 87 |
+
```yaml
|
| 88 |
+
# docker-compose.yml
|
| 89 |
+
services:
|
| 90 |
+
agent:
|
| 91 |
+
volumes:
|
| 92 |
+
- type: tmpfs
|
| 93 |
+
target: /tmp
|
| 94 |
+
tmpfs:
|
| 95 |
+
size: 1G # 临时文件最多 1GB
|
| 96 |
+
- type: volume
|
| 97 |
+
source: agent-data
|
| 98 |
+
target: /app/data
|
| 99 |
+
volume:
|
| 100 |
+
driver_opts:
|
| 101 |
+
size: 5G # 数据卷最多 5GB
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
### 3.3 Linux 账户权限限制
|
| 106 |
+
|
| 107 |
+
#### 3.3.1 最小权限原则
|
| 108 |
+
|
| 109 |
+
**请勿使用 root 用户运行 Agent**,这会带来严重的安全风险。
|
| 110 |
+
|
| 111 |
+
**Dockerfile 最佳实践**:
|
| 112 |
+
```dockerfile
|
| 113 |
+
FROM python:3.11-slim
|
| 114 |
+
|
| 115 |
+
# 安装必要的系统工具
|
| 116 |
+
RUN apt-get update && apt-get install -y \
|
| 117 |
+
git \
|
| 118 |
+
curl \
|
| 119 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 120 |
+
|
| 121 |
+
# 安装 uv
|
| 122 |
+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 123 |
+
ENV PATH="/root/.cargo/bin:$PATH"
|
| 124 |
+
|
| 125 |
+
# 创建非特权用户
|
| 126 |
+
RUN groupadd -r agent && useradd -r -g agent agent
|
| 127 |
+
|
| 128 |
+
# 设置工作目录
|
| 129 |
+
WORKDIR /app
|
| 130 |
+
|
| 131 |
+
# 方案1:从 Git 仓库克隆(适用于公开仓库)
|
| 132 |
+
RUN git clone https://github.com/MiniMax-AI/agent-demo.git . && \
|
| 133 |
+
chown -R agent:agent /app
|
| 134 |
+
|
| 135 |
+
# 方案2:从本地复制代码(适用于私有部署)
|
| 136 |
+
# COPY --chown=agent:agent . /app
|
| 137 |
+
|
| 138 |
+
# 切换到非特权用户后安装依赖
|
| 139 |
+
USER agent
|
| 140 |
+
|
| 141 |
+
# 使用 uv 同步依赖
|
| 142 |
+
RUN uv sync
|
| 143 |
+
|
| 144 |
+
# 启动应用
|
| 145 |
+
CMD ["uv", "run", "mini-agent"]
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
#### 3.3.2 文件系统权限
|
| 149 |
+
|
| 150 |
+
您应限制 Agent 只能访问必要的目录:
|
| 151 |
+
|
| 152 |
+
```bash
|
| 153 |
+
# 创建受限的工作目录
|
| 154 |
+
mkdir -p /app/workspace
|
| 155 |
+
chown agent:agent /app/workspace
|
| 156 |
+
chmod 750 /app/workspace # 所有者读写执行,组只读执行
|
| 157 |
+
|
| 158 |
+
# 限制敏感目录的访问
|
| 159 |
+
chmod 700 /etc/agent # 配置目录只有所有者能访问
|
| 160 |
+
chmod 600 /etc/agent/*.yaml # 配置文件只有所有者能读写
|
| 161 |
+
```
|
docs/assets/demo1-task-execution.gif
ADDED
|
Git LFS Details
|
docs/assets/demo2-claude-skill.gif
ADDED
|
Git LFS Details
|
docs/assets/demo3-web-search.gif
ADDED
|
Git LFS Details
|
examples/01_basic_tools.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Example 1: Basic Tools Usage
|
| 2 |
+
|
| 3 |
+
This example demonstrates how to use the basic tools:
|
| 4 |
+
- ReadTool: Read file contents
|
| 5 |
+
- WriteTool: Create new files
|
| 6 |
+
- EditTool: Edit existing files
|
| 7 |
+
- BashTool: Execute bash commands
|
| 8 |
+
|
| 9 |
+
Based on: tests/test_tools.py
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import asyncio
|
| 13 |
+
import tempfile
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
from mini_agent.tools import BashTool, EditTool, ReadTool, WriteTool
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
async def demo_write_tool():
|
| 20 |
+
"""Demo: Write a new file."""
|
| 21 |
+
print("\n" + "=" * 60)
|
| 22 |
+
print("Demo 1: WriteTool - Create a new file")
|
| 23 |
+
print("=" * 60)
|
| 24 |
+
|
| 25 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 26 |
+
file_path = Path(tmpdir) / "hello.txt"
|
| 27 |
+
|
| 28 |
+
tool = WriteTool()
|
| 29 |
+
result = await tool.execute(
|
| 30 |
+
path=str(file_path), content="Hello, Mini Agent!\nThis is a test file."
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
if result.success:
|
| 34 |
+
print(f"✅ File created: {file_path}")
|
| 35 |
+
print(f"Content:\n{file_path.read_text()}")
|
| 36 |
+
else:
|
| 37 |
+
print(f"❌ Failed: {result.error}")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
async def demo_read_tool():
|
| 41 |
+
"""Demo: Read a file."""
|
| 42 |
+
print("\n" + "=" * 60)
|
| 43 |
+
print("Demo 2: ReadTool - Read file contents")
|
| 44 |
+
print("=" * 60)
|
| 45 |
+
|
| 46 |
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f:
|
| 47 |
+
f.write("Line 1: Hello\nLine 2: World\nLine 3: Mini Agent")
|
| 48 |
+
temp_path = f.name
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
tool = ReadTool()
|
| 52 |
+
result = await tool.execute(path=temp_path)
|
| 53 |
+
|
| 54 |
+
if result.success:
|
| 55 |
+
print(f"✅ File read successfully")
|
| 56 |
+
print(f"Content:\n{result.content}")
|
| 57 |
+
else:
|
| 58 |
+
print(f"❌ Failed: {result.error}")
|
| 59 |
+
finally:
|
| 60 |
+
Path(temp_path).unlink()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
async def demo_edit_tool():
|
| 64 |
+
"""Demo: Edit an existing file."""
|
| 65 |
+
print("\n" + "=" * 60)
|
| 66 |
+
print("Demo 3: EditTool - Edit file content")
|
| 67 |
+
print("=" * 60)
|
| 68 |
+
|
| 69 |
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as f:
|
| 70 |
+
f.write("Python is great!\nI love Python programming.")
|
| 71 |
+
temp_path = f.name
|
| 72 |
+
|
| 73 |
+
try:
|
| 74 |
+
print(f"Original content:\n{Path(temp_path).read_text()}\n")
|
| 75 |
+
|
| 76 |
+
tool = EditTool()
|
| 77 |
+
result = await tool.execute(
|
| 78 |
+
path=temp_path, old_str="Python", new_str="Agent"
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
if result.success:
|
| 82 |
+
print(f"✅ File edited successfully")
|
| 83 |
+
print(f"New content:\n{Path(temp_path).read_text()}")
|
| 84 |
+
else:
|
| 85 |
+
print(f"❌ Failed: {result.error}")
|
| 86 |
+
finally:
|
| 87 |
+
Path(temp_path).unlink()
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
async def demo_bash_tool():
|
| 91 |
+
"""Demo: Execute bash commands."""
|
| 92 |
+
print("\n" + "=" * 60)
|
| 93 |
+
print("Demo 4: BashTool - Execute bash commands")
|
| 94 |
+
print("=" * 60)
|
| 95 |
+
|
| 96 |
+
tool = BashTool()
|
| 97 |
+
|
| 98 |
+
# Example 1: List files
|
| 99 |
+
print("\nCommand: ls -la")
|
| 100 |
+
result = await tool.execute(command="ls -la")
|
| 101 |
+
if result.success:
|
| 102 |
+
print(f"✅ Command executed successfully")
|
| 103 |
+
print(f"Output:\n{result.content[:200]}...")
|
| 104 |
+
|
| 105 |
+
# Example 2: Get current directory
|
| 106 |
+
print("\nCommand: pwd")
|
| 107 |
+
result = await tool.execute(command="pwd")
|
| 108 |
+
if result.success:
|
| 109 |
+
print(f"✅ Current directory: {result.content.strip()}")
|
| 110 |
+
|
| 111 |
+
# Example 3: Echo
|
| 112 |
+
print("\nCommand: echo 'Hello from BashTool!'")
|
| 113 |
+
result = await tool.execute(command="echo 'Hello from BashTool!'")
|
| 114 |
+
if result.success:
|
| 115 |
+
print(f"✅ Output: {result.content.strip()}")
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
async def main():
|
| 119 |
+
"""Run all demos."""
|
| 120 |
+
print("=" * 60)
|
| 121 |
+
print("Basic Tools Usage Examples")
|
| 122 |
+
print("=" * 60)
|
| 123 |
+
print("\nThese examples show how to use the core tools directly.")
|
| 124 |
+
print("In a real agent scenario, the LLM decides which tools to use.\n")
|
| 125 |
+
|
| 126 |
+
await demo_write_tool()
|
| 127 |
+
await demo_read_tool()
|
| 128 |
+
await demo_edit_tool()
|
| 129 |
+
await demo_bash_tool()
|
| 130 |
+
|
| 131 |
+
print("\n" + "=" * 60)
|
| 132 |
+
print("All demos completed! ✅")
|
| 133 |
+
print("=" * 60)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
if __name__ == "__main__":
|
| 137 |
+
asyncio.run(main())
|
examples/02_simple_agent.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Example 2: Simple Agent Usage
|
| 2 |
+
|
| 3 |
+
This example demonstrates how to create and run a basic agent
|
| 4 |
+
to perform simple file operations.
|
| 5 |
+
|
| 6 |
+
Based on: tests/test_agent.py
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import asyncio
|
| 10 |
+
import tempfile
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
from mini_agent import LLMClient
|
| 14 |
+
from mini_agent.agent import Agent
|
| 15 |
+
from mini_agent.config import Config
|
| 16 |
+
from mini_agent.tools import BashTool, EditTool, ReadTool, WriteTool
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
async def demo_file_creation():
|
| 20 |
+
"""Demo: Agent creates a file based on user request."""
|
| 21 |
+
print("\n" + "=" * 60)
|
| 22 |
+
print("Demo: Agent-Driven File Creation")
|
| 23 |
+
print("=" * 60)
|
| 24 |
+
|
| 25 |
+
# Load configuration
|
| 26 |
+
config_path = Path("mini_agent/config/config.yaml")
|
| 27 |
+
if not config_path.exists():
|
| 28 |
+
print("❌ config.yaml not found. Please set up your API key first.")
|
| 29 |
+
print(" Run: cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml")
|
| 30 |
+
return
|
| 31 |
+
|
| 32 |
+
config = Config.from_yaml(config_path)
|
| 33 |
+
|
| 34 |
+
# Check API key
|
| 35 |
+
if not config.llm.api_key or config.llm.api_key.startswith("YOUR_"):
|
| 36 |
+
print("❌ API key not configured in config.yaml")
|
| 37 |
+
return
|
| 38 |
+
|
| 39 |
+
# Create temporary workspace
|
| 40 |
+
with tempfile.TemporaryDirectory() as workspace_dir:
|
| 41 |
+
print(f"📁 Workspace: {workspace_dir}\n")
|
| 42 |
+
|
| 43 |
+
# Load system prompt (Agent will auto-inject workspace info)
|
| 44 |
+
system_prompt_path = Path("mini_agent/config/system_prompt.md")
|
| 45 |
+
if system_prompt_path.exists():
|
| 46 |
+
system_prompt = system_prompt_path.read_text(encoding="utf-8")
|
| 47 |
+
else:
|
| 48 |
+
system_prompt = "You are a helpful AI assistant that can use tools."
|
| 49 |
+
|
| 50 |
+
# Initialize LLM client
|
| 51 |
+
llm_client = LLMClient(
|
| 52 |
+
api_key=config.llm.api_key,
|
| 53 |
+
api_base=config.llm.api_base,
|
| 54 |
+
model=config.llm.model,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# Initialize tools
|
| 58 |
+
tools = [
|
| 59 |
+
ReadTool(workspace_dir=workspace_dir),
|
| 60 |
+
WriteTool(workspace_dir=workspace_dir),
|
| 61 |
+
EditTool(workspace_dir=workspace_dir),
|
| 62 |
+
BashTool(),
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
# Create agent
|
| 66 |
+
agent = Agent(
|
| 67 |
+
llm_client=llm_client,
|
| 68 |
+
system_prompt=system_prompt,
|
| 69 |
+
tools=tools,
|
| 70 |
+
max_steps=10,
|
| 71 |
+
workspace_dir=workspace_dir,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
# Task: Create a Python hello world file
|
| 75 |
+
task = """
|
| 76 |
+
Create a Python file named 'hello.py' that:
|
| 77 |
+
1. Defines a function called greet(name)
|
| 78 |
+
2. The function prints "Hello, {name}!"
|
| 79 |
+
3. Calls the function with name="Mini Agent"
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
print("📝 Task:")
|
| 83 |
+
print(task)
|
| 84 |
+
print("\n" + "=" * 60)
|
| 85 |
+
print("🤖 Agent is working...\n")
|
| 86 |
+
|
| 87 |
+
agent.add_user_message(task)
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
result = await agent.run()
|
| 91 |
+
|
| 92 |
+
print("\n" + "=" * 60)
|
| 93 |
+
print("✅ Agent completed the task!")
|
| 94 |
+
print("=" * 60)
|
| 95 |
+
print(f"\nAgent's response:\n{result}\n")
|
| 96 |
+
|
| 97 |
+
# Check if file was created
|
| 98 |
+
hello_file = Path(workspace_dir) / "hello.py"
|
| 99 |
+
if hello_file.exists():
|
| 100 |
+
print("=" * 60)
|
| 101 |
+
print("📄 Created file content:")
|
| 102 |
+
print("=" * 60)
|
| 103 |
+
print(hello_file.read_text())
|
| 104 |
+
print("=" * 60)
|
| 105 |
+
else:
|
| 106 |
+
print("⚠️ File was not created (but agent may have completed differently)")
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
print(f"❌ Error: {e}")
|
| 110 |
+
import traceback
|
| 111 |
+
|
| 112 |
+
traceback.print_exc()
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
async def demo_bash_task():
|
| 116 |
+
"""Demo: Agent executes bash commands."""
|
| 117 |
+
print("\n" + "=" * 60)
|
| 118 |
+
print("Demo: Agent-Driven Bash Commands")
|
| 119 |
+
print("=" * 60)
|
| 120 |
+
|
| 121 |
+
# Load configuration
|
| 122 |
+
config_path = Path("mini_agent/config/config.yaml")
|
| 123 |
+
if not config_path.exists():
|
| 124 |
+
print("❌ config.yaml not found")
|
| 125 |
+
return
|
| 126 |
+
|
| 127 |
+
config = Config.from_yaml(config_path)
|
| 128 |
+
|
| 129 |
+
if not config.llm.api_key or config.llm.api_key.startswith("YOUR_"):
|
| 130 |
+
print("❌ API key not configured")
|
| 131 |
+
return
|
| 132 |
+
|
| 133 |
+
with tempfile.TemporaryDirectory() as workspace_dir:
|
| 134 |
+
print(f"📁 Workspace: {workspace_dir}\n")
|
| 135 |
+
|
| 136 |
+
# Load system prompt (Agent will auto-inject workspace info)
|
| 137 |
+
system_prompt_path = Path("mini_agent/config/system_prompt.md")
|
| 138 |
+
if system_prompt_path.exists():
|
| 139 |
+
system_prompt = system_prompt_path.read_text(encoding="utf-8")
|
| 140 |
+
else:
|
| 141 |
+
system_prompt = "You are a helpful AI assistant that can use tools."
|
| 142 |
+
|
| 143 |
+
# Initialize LLM
|
| 144 |
+
llm_client = LLMClient(
|
| 145 |
+
api_key=config.llm.api_key,
|
| 146 |
+
api_base=config.llm.api_base,
|
| 147 |
+
model=config.llm.model,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
# Tools
|
| 151 |
+
tools = [
|
| 152 |
+
ReadTool(workspace_dir=workspace_dir),
|
| 153 |
+
WriteTool(workspace_dir=workspace_dir),
|
| 154 |
+
BashTool(),
|
| 155 |
+
]
|
| 156 |
+
|
| 157 |
+
# Create agent
|
| 158 |
+
agent = Agent(
|
| 159 |
+
llm_client=llm_client,
|
| 160 |
+
system_prompt=system_prompt,
|
| 161 |
+
tools=tools,
|
| 162 |
+
max_steps=10,
|
| 163 |
+
workspace_dir=workspace_dir,
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
# Task: Use bash to get system info
|
| 167 |
+
task = """
|
| 168 |
+
Use bash commands to:
|
| 169 |
+
1. Show the current date and time
|
| 170 |
+
2. List all Python files in the current directory
|
| 171 |
+
3. Count how many Python files exist
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
print("📝 Task:")
|
| 175 |
+
print(task)
|
| 176 |
+
print("\n" + "=" * 60)
|
| 177 |
+
print("🤖 Agent is working...\n")
|
| 178 |
+
|
| 179 |
+
agent.add_user_message(task)
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
result = await agent.run()
|
| 183 |
+
|
| 184 |
+
print("\n" + "=" * 60)
|
| 185 |
+
print("✅ Agent completed!")
|
| 186 |
+
print("=" * 60)
|
| 187 |
+
print(f"\nAgent's response:\n{result}\n")
|
| 188 |
+
|
| 189 |
+
except Exception as e:
|
| 190 |
+
print(f"❌ Error: {e}")
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
async def main():
|
| 194 |
+
"""Run all demos."""
|
| 195 |
+
print("=" * 60)
|
| 196 |
+
print("Simple Agent Usage Examples")
|
| 197 |
+
print("=" * 60)
|
| 198 |
+
print("\nThese examples show how to create an agent and give it tasks.")
|
| 199 |
+
print("The agent uses LLM to decide which tools to call.\n")
|
| 200 |
+
|
| 201 |
+
# Run demos
|
| 202 |
+
await demo_file_creation()
|
| 203 |
+
print("\n" * 2)
|
| 204 |
+
await demo_bash_task()
|
| 205 |
+
|
| 206 |
+
print("\n" + "=" * 60)
|
| 207 |
+
print("All demos completed! ✅")
|
| 208 |
+
print("=" * 60)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
if __name__ == "__main__":
|
| 212 |
+
asyncio.run(main())
|
examples/03_session_notes.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Example 3: Session Note Tool Usage
|
| 2 |
+
|
| 3 |
+
This example demonstrates the Session Note Tool - one of the core features
|
| 4 |
+
that allows agents to maintain memory across sessions.
|
| 5 |
+
|
| 6 |
+
Based on: tests/test_note_tool.py, tests/test_integration.py
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import asyncio
|
| 10 |
+
import json
|
| 11 |
+
import tempfile
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
from mini_agent import LLMClient
|
| 15 |
+
from mini_agent.agent import Agent
|
| 16 |
+
from mini_agent.config import Config
|
| 17 |
+
from mini_agent.tools import BashTool, ReadTool, WriteTool
|
| 18 |
+
from mini_agent.tools.note_tool import RecallNoteTool, SessionNoteTool
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
async def demo_direct_note_usage():
|
| 22 |
+
"""Demo: Direct usage of Session Note tools."""
|
| 23 |
+
print("\n" + "=" * 60)
|
| 24 |
+
print("Demo 1: Direct Session Note Tool Usage")
|
| 25 |
+
print("=" * 60)
|
| 26 |
+
|
| 27 |
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f:
|
| 28 |
+
note_file = f.name
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
# Create tools
|
| 32 |
+
record_tool = SessionNoteTool(memory_file=note_file)
|
| 33 |
+
recall_tool = RecallNoteTool(memory_file=note_file)
|
| 34 |
+
|
| 35 |
+
# Record some notes
|
| 36 |
+
print("\n📝 Recording notes...")
|
| 37 |
+
|
| 38 |
+
result = await record_tool.execute(
|
| 39 |
+
content="User is a Python developer working on agent systems",
|
| 40 |
+
category="user_info",
|
| 41 |
+
)
|
| 42 |
+
print(f" ✓ {result.content}")
|
| 43 |
+
|
| 44 |
+
result = await record_tool.execute(
|
| 45 |
+
content="Project name: mini-agent, Tech: Python 3.12 + async",
|
| 46 |
+
category="project_info",
|
| 47 |
+
)
|
| 48 |
+
print(f" ✓ {result.content}")
|
| 49 |
+
|
| 50 |
+
result = await record_tool.execute(
|
| 51 |
+
content="User prefers concise, well-documented code",
|
| 52 |
+
category="user_preference",
|
| 53 |
+
)
|
| 54 |
+
print(f" ✓ {result.content}")
|
| 55 |
+
|
| 56 |
+
# Recall all notes
|
| 57 |
+
print("\n🔍 Recalling all notes...")
|
| 58 |
+
result = await recall_tool.execute()
|
| 59 |
+
print(result.content)
|
| 60 |
+
|
| 61 |
+
# Recall filtered notes
|
| 62 |
+
print("\n🔍 Recalling user preferences only...")
|
| 63 |
+
result = await recall_tool.execute(category="user_preference")
|
| 64 |
+
print(result.content)
|
| 65 |
+
|
| 66 |
+
# Show the memory file
|
| 67 |
+
print("\n📄 Memory file content:")
|
| 68 |
+
print("=" * 60)
|
| 69 |
+
notes = json.loads(Path(note_file).read_text())
|
| 70 |
+
print(json.dumps(notes, indent=2, ensure_ascii=False))
|
| 71 |
+
print("=" * 60)
|
| 72 |
+
|
| 73 |
+
finally:
|
| 74 |
+
Path(note_file).unlink(missing_ok=True)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
async def demo_agent_with_notes():
|
| 78 |
+
"""Demo: Agent using Session Notes to remember context."""
|
| 79 |
+
print("\n" + "=" * 60)
|
| 80 |
+
print("Demo 2: Agent with Session Memory")
|
| 81 |
+
print("=" * 60)
|
| 82 |
+
|
| 83 |
+
# Load configuration
|
| 84 |
+
config_path = Path("mini_agent/config/config.yaml")
|
| 85 |
+
if not config_path.exists():
|
| 86 |
+
print("❌ config.yaml not found")
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
+
config = Config.from_yaml(config_path)
|
| 90 |
+
|
| 91 |
+
if not config.llm.api_key or config.llm.api_key.startswith("YOUR_"):
|
| 92 |
+
print("❌ API key not configured")
|
| 93 |
+
return
|
| 94 |
+
|
| 95 |
+
with tempfile.TemporaryDirectory() as workspace_dir:
|
| 96 |
+
print(f"📁 Workspace: {workspace_dir}\n")
|
| 97 |
+
|
| 98 |
+
# Load system prompt (Agent will auto-inject workspace info)
|
| 99 |
+
system_prompt_path = Path("mini_agent/config/system_prompt.md")
|
| 100 |
+
if system_prompt_path.exists():
|
| 101 |
+
system_prompt = system_prompt_path.read_text(encoding="utf-8")
|
| 102 |
+
else:
|
| 103 |
+
system_prompt = "You are a helpful AI assistant."
|
| 104 |
+
|
| 105 |
+
# Add Session Note instructions
|
| 106 |
+
note_instructions = """
|
| 107 |
+
|
| 108 |
+
IMPORTANT - Session Note Management:
|
| 109 |
+
You have access to record_note and recall_notes tools. Use them to:
|
| 110 |
+
- record_note: Save important facts, preferences, decisions that should persist
|
| 111 |
+
- recall_notes: Retrieve previously saved notes
|
| 112 |
+
|
| 113 |
+
Guidelines:
|
| 114 |
+
- Proactively record key information during conversations
|
| 115 |
+
- Recall notes at the start to restore context
|
| 116 |
+
- Categories: user_info, user_preference, project_info, decision, etc.
|
| 117 |
+
"""
|
| 118 |
+
system_prompt += note_instructions
|
| 119 |
+
|
| 120 |
+
# Initialize LLM
|
| 121 |
+
llm_client = LLMClient(
|
| 122 |
+
api_key=config.llm.api_key,
|
| 123 |
+
api_base=config.llm.api_base,
|
| 124 |
+
model=config.llm.model,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
# Memory file
|
| 128 |
+
memory_file = Path(workspace_dir) / ".agent_memory.json"
|
| 129 |
+
|
| 130 |
+
# Tools including Session Note tools
|
| 131 |
+
tools = [
|
| 132 |
+
ReadTool(workspace_dir=workspace_dir),
|
| 133 |
+
WriteTool(workspace_dir=workspace_dir),
|
| 134 |
+
BashTool(),
|
| 135 |
+
SessionNoteTool(memory_file=str(memory_file)),
|
| 136 |
+
RecallNoteTool(memory_file=str(memory_file)),
|
| 137 |
+
]
|
| 138 |
+
|
| 139 |
+
# === First Session ===
|
| 140 |
+
print("=" * 60)
|
| 141 |
+
print("Session 1: Teaching the agent about user preferences")
|
| 142 |
+
print("=" * 60)
|
| 143 |
+
|
| 144 |
+
agent1 = Agent(
|
| 145 |
+
llm_client=llm_client,
|
| 146 |
+
system_prompt=system_prompt,
|
| 147 |
+
tools=tools,
|
| 148 |
+
max_steps=15,
|
| 149 |
+
workspace_dir=workspace_dir,
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
task1 = """
|
| 153 |
+
Hello! Let me introduce myself:
|
| 154 |
+
- I'm Alex, a senior Python developer
|
| 155 |
+
- I'm building an AI agent framework called "mini-agent"
|
| 156 |
+
- I use Python 3.12 with asyncio
|
| 157 |
+
- I prefer type hints and comprehensive docstrings
|
| 158 |
+
- My coding style: clean, functional, well-tested
|
| 159 |
+
|
| 160 |
+
Please remember this information for future conversations.
|
| 161 |
+
Also, create a simple README.md file acknowledging you understood.
|
| 162 |
+
"""
|
| 163 |
+
|
| 164 |
+
print(f"\n📝 User message:\n{task1}\n")
|
| 165 |
+
print("🤖 Agent is working...\n")
|
| 166 |
+
|
| 167 |
+
agent1.add_user_message(task1)
|
| 168 |
+
|
| 169 |
+
try:
|
| 170 |
+
result1 = await agent1.run()
|
| 171 |
+
print("\n" + "=" * 60)
|
| 172 |
+
print("Agent response:")
|
| 173 |
+
print("=" * 60)
|
| 174 |
+
print(result1)
|
| 175 |
+
print("=" * 60)
|
| 176 |
+
|
| 177 |
+
# Check memory file
|
| 178 |
+
if memory_file.exists():
|
| 179 |
+
notes = json.loads(memory_file.read_text())
|
| 180 |
+
print(f"\n✅ Agent recorded {len(notes)} notes in memory")
|
| 181 |
+
for note in notes:
|
| 182 |
+
print(f" - [{note['category']}] {note['content'][:50]}...")
|
| 183 |
+
else:
|
| 184 |
+
print("\n⚠️ No notes found")
|
| 185 |
+
|
| 186 |
+
except Exception as e:
|
| 187 |
+
print(f"❌ Error: {e}")
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
# === Second Session (New Agent Instance) ===
|
| 191 |
+
print("\n\n" + "=" * 60)
|
| 192 |
+
print("Session 2: New agent instance (simulating new conversation)")
|
| 193 |
+
print("=" * 60)
|
| 194 |
+
|
| 195 |
+
agent2 = Agent(
|
| 196 |
+
llm_client=llm_client,
|
| 197 |
+
system_prompt=system_prompt,
|
| 198 |
+
tools=tools,
|
| 199 |
+
max_steps=10,
|
| 200 |
+
workspace_dir=workspace_dir,
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
task2 = """
|
| 204 |
+
Hello! I'm back. Do you remember who I am and what project I'm working on?
|
| 205 |
+
What were my code style preferences?
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
print(f"\n📝 User message:\n{task2}\n")
|
| 209 |
+
print("🤖 Agent is working (should recall previous notes)...\n")
|
| 210 |
+
|
| 211 |
+
agent2.add_user_message(task2)
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
result2 = await agent2.run()
|
| 215 |
+
print("\n" + "=" * 60)
|
| 216 |
+
print("Agent response:")
|
| 217 |
+
print("=" * 60)
|
| 218 |
+
print(result2)
|
| 219 |
+
print("=" * 60)
|
| 220 |
+
|
| 221 |
+
print("\n✅ Session Note Demo completed!")
|
| 222 |
+
print("\nKey Points:")
|
| 223 |
+
print(" 1. Agent in Session 1 recorded important information")
|
| 224 |
+
print(" 2. Agent in Session 2 recalled previous notes")
|
| 225 |
+
print(" 3. Memory persists across agent instances via file")
|
| 226 |
+
|
| 227 |
+
except Exception as e:
|
| 228 |
+
print(f"❌ Error: {e}")
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
async def main():
|
| 232 |
+
"""Run all demos."""
|
| 233 |
+
print("=" * 60)
|
| 234 |
+
print("Session Note Tool Examples")
|
| 235 |
+
print("=" * 60)
|
| 236 |
+
print("\nSession Notes allow agents to remember context across sessions.")
|
| 237 |
+
print("This is a key feature for building production-ready agents.\n")
|
| 238 |
+
|
| 239 |
+
# Run demos
|
| 240 |
+
await demo_direct_note_usage()
|
| 241 |
+
print("\n" * 2)
|
| 242 |
+
await demo_agent_with_notes()
|
| 243 |
+
|
| 244 |
+
print("\n" + "=" * 60)
|
| 245 |
+
print("All demos completed! ✅")
|
| 246 |
+
print("=" * 60)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
if __name__ == "__main__":
|
| 250 |
+
asyncio.run(main())
|
examples/04_full_agent.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Example 4: Full Agent with All Features
|
| 2 |
+
|
| 3 |
+
This example demonstrates a complete agent setup with:
|
| 4 |
+
- All basic tools (Read, Write, Edit, Bash)
|
| 5 |
+
- Session Note tools for memory
|
| 6 |
+
- MCP tools (Memory, Search, etc.)
|
| 7 |
+
- Skills integration
|
| 8 |
+
|
| 9 |
+
Based on: tests/test_integration.py
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import asyncio
|
| 13 |
+
import tempfile
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
from mini_agent import LLMClient
|
| 17 |
+
from mini_agent.agent import Agent
|
| 18 |
+
from mini_agent.config import Config
|
| 19 |
+
from mini_agent.tools import BashTool, EditTool, ReadTool, WriteTool
|
| 20 |
+
from mini_agent.tools.mcp_loader import load_mcp_tools_async
|
| 21 |
+
from mini_agent.tools.note_tool import RecallNoteTool, SessionNoteTool
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
async def demo_full_agent():
|
| 25 |
+
"""Demo: Full-featured agent with all capabilities."""
|
| 26 |
+
print("\n" + "=" * 60)
|
| 27 |
+
print("Full Mini Agent - All Features Enabled")
|
| 28 |
+
print("=" * 60)
|
| 29 |
+
|
| 30 |
+
# Load configuration
|
| 31 |
+
config_path = Path("mini_agent/config/config.yaml")
|
| 32 |
+
if not config_path.exists():
|
| 33 |
+
print("❌ config.yaml not found. Please run:")
|
| 34 |
+
print(" cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml")
|
| 35 |
+
return
|
| 36 |
+
|
| 37 |
+
config = Config.from_yaml(config_path)
|
| 38 |
+
|
| 39 |
+
# Check API key
|
| 40 |
+
if not config.llm.api_key or config.llm.api_key.startswith("YOUR_"):
|
| 41 |
+
print("❌ API key not configured in config.yaml")
|
| 42 |
+
return
|
| 43 |
+
|
| 44 |
+
# Create workspace
|
| 45 |
+
with tempfile.TemporaryDirectory() as workspace_dir:
|
| 46 |
+
print(f"📁 Workspace: {workspace_dir}")
|
| 47 |
+
|
| 48 |
+
# Load system prompt (Agent will auto-inject workspace info)
|
| 49 |
+
system_prompt_path = Path("mini_agent/config/system_prompt.md")
|
| 50 |
+
if system_prompt_path.exists():
|
| 51 |
+
system_prompt = system_prompt_path.read_text(encoding="utf-8")
|
| 52 |
+
else:
|
| 53 |
+
system_prompt = "You are a helpful AI assistant."
|
| 54 |
+
|
| 55 |
+
# Add Session Note instructions
|
| 56 |
+
note_instructions = """
|
| 57 |
+
|
| 58 |
+
IMPORTANT - Session Memory:
|
| 59 |
+
You have record_note and recall_notes tools. Use them to:
|
| 60 |
+
- Save important facts, decisions, and context
|
| 61 |
+
- Recall previous information across conversations
|
| 62 |
+
"""
|
| 63 |
+
system_prompt += note_instructions
|
| 64 |
+
|
| 65 |
+
# Initialize LLM
|
| 66 |
+
llm_client = LLMClient(
|
| 67 |
+
api_key=config.llm.api_key,
|
| 68 |
+
api_base=config.llm.api_base,
|
| 69 |
+
model=config.llm.model,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# Initialize basic tools
|
| 73 |
+
tools = [
|
| 74 |
+
ReadTool(workspace_dir=workspace_dir),
|
| 75 |
+
WriteTool(workspace_dir=workspace_dir),
|
| 76 |
+
EditTool(workspace_dir=workspace_dir),
|
| 77 |
+
BashTool(),
|
| 78 |
+
]
|
| 79 |
+
print("✓ Loaded 4 basic tools")
|
| 80 |
+
|
| 81 |
+
# Add Session Note tools
|
| 82 |
+
memory_file = Path(workspace_dir) / ".agent_memory.json"
|
| 83 |
+
tools.extend(
|
| 84 |
+
[
|
| 85 |
+
SessionNoteTool(memory_file=str(memory_file)),
|
| 86 |
+
RecallNoteTool(memory_file=str(memory_file)),
|
| 87 |
+
]
|
| 88 |
+
)
|
| 89 |
+
print("✓ Loaded 2 Session Note tools")
|
| 90 |
+
|
| 91 |
+
# Load MCP tools (if configured)
|
| 92 |
+
try:
|
| 93 |
+
mcp_tools = await load_mcp_tools_async(config_path="mini_agent/config/mcp.json")
|
| 94 |
+
if mcp_tools:
|
| 95 |
+
tools.extend(mcp_tools)
|
| 96 |
+
print(f"✓ Loaded {len(mcp_tools)} MCP tools")
|
| 97 |
+
else:
|
| 98 |
+
print("⚠️ No MCP tools configured (mcp.json is empty or disabled)")
|
| 99 |
+
except Exception as e:
|
| 100 |
+
print(f"⚠️ MCP tools not loaded: {e}")
|
| 101 |
+
|
| 102 |
+
# Create agent
|
| 103 |
+
agent = Agent(
|
| 104 |
+
llm_client=llm_client,
|
| 105 |
+
system_prompt=system_prompt,
|
| 106 |
+
tools=tools,
|
| 107 |
+
max_steps=config.agent.max_steps,
|
| 108 |
+
workspace_dir=workspace_dir,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
print(f"\n🤖 Agent created with {len(tools)} total tools\n")
|
| 112 |
+
|
| 113 |
+
# Task: Complex task that uses multiple tools
|
| 114 |
+
task = """
|
| 115 |
+
Please help me with the following tasks:
|
| 116 |
+
|
| 117 |
+
1. Create a Python script called 'calculator.py' that:
|
| 118 |
+
- Has functions for add, subtract, multiply, divide
|
| 119 |
+
- Has a main() function that demonstrates usage
|
| 120 |
+
- Includes proper docstrings and type hints
|
| 121 |
+
|
| 122 |
+
2. Create a README.md file that:
|
| 123 |
+
- Describes the calculator script
|
| 124 |
+
- Shows how to run it
|
| 125 |
+
- Lists the available functions
|
| 126 |
+
|
| 127 |
+
3. Test the calculator by running it with bash
|
| 128 |
+
|
| 129 |
+
4. Remember this project info:
|
| 130 |
+
- Project: Simple Calculator
|
| 131 |
+
- Language: Python
|
| 132 |
+
- Purpose: Demonstration of agent capabilities
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
print("=" * 60)
|
| 136 |
+
print("📝 Task:")
|
| 137 |
+
print("=" * 60)
|
| 138 |
+
print(task)
|
| 139 |
+
print("\n" + "=" * 60)
|
| 140 |
+
print("🤖 Agent is working...\n")
|
| 141 |
+
|
| 142 |
+
agent.add_user_message(task)
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
result = await agent.run()
|
| 146 |
+
|
| 147 |
+
print("\n" + "=" * 60)
|
| 148 |
+
print("✅ Agent completed!")
|
| 149 |
+
print("=" * 60)
|
| 150 |
+
print(f"\nAgent's final response:\n{result}\n")
|
| 151 |
+
|
| 152 |
+
# Show created files
|
| 153 |
+
print("=" * 60)
|
| 154 |
+
print("��� Created files in workspace:")
|
| 155 |
+
print("=" * 60)
|
| 156 |
+
|
| 157 |
+
workspace = Path(workspace_dir)
|
| 158 |
+
for file in workspace.glob("*"):
|
| 159 |
+
if file.is_file() and not file.name.startswith("."):
|
| 160 |
+
print(f"\n📄 {file.name}:")
|
| 161 |
+
print("-" * 60)
|
| 162 |
+
content = file.read_text()
|
| 163 |
+
# Show first 20 lines
|
| 164 |
+
lines = content.split("\n")[:20]
|
| 165 |
+
print("\n".join(lines))
|
| 166 |
+
if len(content.split("\n")) > 20:
|
| 167 |
+
print("... (truncated)")
|
| 168 |
+
print("-" * 60)
|
| 169 |
+
|
| 170 |
+
# Show memory
|
| 171 |
+
if memory_file.exists():
|
| 172 |
+
import json
|
| 173 |
+
|
| 174 |
+
notes = json.loads(memory_file.read_text())
|
| 175 |
+
print(f"\n💾 Session notes recorded: {len(notes)}")
|
| 176 |
+
for note in notes:
|
| 177 |
+
print(f" - [{note['category']}] {note['content'][:60]}...")
|
| 178 |
+
|
| 179 |
+
except Exception as e:
|
| 180 |
+
print(f"❌ Error during agent execution: {e}")
|
| 181 |
+
import traceback
|
| 182 |
+
|
| 183 |
+
traceback.print_exc()
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
async def demo_interactive_mode():
|
| 187 |
+
"""Demo: Interactive conversation with agent."""
|
| 188 |
+
print("\n" + "=" * 60)
|
| 189 |
+
print("Interactive Mini Agent")
|
| 190 |
+
print("=" * 60)
|
| 191 |
+
print("\nThis demo shows multi-turn conversation.")
|
| 192 |
+
print("(In production, use `mini-agent` for full interactive mode)")
|
| 193 |
+
|
| 194 |
+
# Load config
|
| 195 |
+
config_path = Path("mini_agent/config/config.yaml")
|
| 196 |
+
if not config_path.exists():
|
| 197 |
+
print("❌ config.yaml not found")
|
| 198 |
+
return
|
| 199 |
+
|
| 200 |
+
config = Config.from_yaml(config_path)
|
| 201 |
+
|
| 202 |
+
if not config.llm.api_key or config.llm.api_key.startswith("YOUR_"):
|
| 203 |
+
print("❌ API key not configured")
|
| 204 |
+
return
|
| 205 |
+
|
| 206 |
+
with tempfile.TemporaryDirectory() as workspace_dir:
|
| 207 |
+
# Setup
|
| 208 |
+
system_prompt = "You are a helpful assistant with access to tools."
|
| 209 |
+
llm_client = LLMClient(
|
| 210 |
+
api_key=config.llm.api_key,
|
| 211 |
+
api_base=config.llm.api_base,
|
| 212 |
+
model=config.llm.model,
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
tools = [
|
| 216 |
+
WriteTool(workspace_dir=workspace_dir),
|
| 217 |
+
ReadTool(workspace_dir=workspace_dir),
|
| 218 |
+
BashTool(),
|
| 219 |
+
]
|
| 220 |
+
|
| 221 |
+
agent = Agent(
|
| 222 |
+
llm_client=llm_client,
|
| 223 |
+
system_prompt=system_prompt,
|
| 224 |
+
tools=tools,
|
| 225 |
+
max_steps=20,
|
| 226 |
+
workspace_dir=workspace_dir,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
# Conversation turns
|
| 230 |
+
conversations = [
|
| 231 |
+
"Create a file called 'data.txt' with the numbers 1 to 5, one per line.",
|
| 232 |
+
"Now read the file and tell me what's in it.",
|
| 233 |
+
"Count how many lines are in the file using bash.",
|
| 234 |
+
]
|
| 235 |
+
|
| 236 |
+
for i, message in enumerate(conversations, 1):
|
| 237 |
+
print(f"\n{'=' * 60}")
|
| 238 |
+
print(f"Turn {i}:")
|
| 239 |
+
print(f"{'=' * 60}")
|
| 240 |
+
print(f"User: {message}\n")
|
| 241 |
+
|
| 242 |
+
agent.add_user_message(message)
|
| 243 |
+
|
| 244 |
+
try:
|
| 245 |
+
result = await agent.run()
|
| 246 |
+
print(f"Agent: {result}\n")
|
| 247 |
+
except Exception as e:
|
| 248 |
+
print(f"Error: {e}")
|
| 249 |
+
break
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
async def main():
|
| 253 |
+
"""Run all demos."""
|
| 254 |
+
print("=" * 60)
|
| 255 |
+
print("Full Agent Examples")
|
| 256 |
+
print("=" * 60)
|
| 257 |
+
print("\nThese examples demonstrate the complete agent capabilities:")
|
| 258 |
+
print("- All basic tools (file operations, bash)")
|
| 259 |
+
print("- Session memory (persistent notes)")
|
| 260 |
+
print("- MCP tools (if configured)")
|
| 261 |
+
print("- Multi-turn conversations\n")
|
| 262 |
+
|
| 263 |
+
# Run demos
|
| 264 |
+
await demo_full_agent()
|
| 265 |
+
print("\n" * 2)
|
| 266 |
+
await demo_interactive_mode()
|
| 267 |
+
|
| 268 |
+
print("\n" + "=" * 60)
|
| 269 |
+
print("All demos completed! ✅")
|
| 270 |
+
print("=" * 60)
|
| 271 |
+
print("\n💡 Next step: Try the interactive mode with:")
|
| 272 |
+
print(" mini-agent\n")
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
if __name__ == "__main__":
|
| 276 |
+
asyncio.run(main())
|
examples/05_provider_selection.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Example: Using LLMClient with different providers.
|
| 2 |
+
|
| 3 |
+
This example demonstrates how to use the LLMClient wrapper with different
|
| 4 |
+
LLM providers (Anthropic or OpenAI) through the provider parameter.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import yaml
|
| 12 |
+
|
| 13 |
+
from mini_agent import LLMClient, LLMProvider, Message
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
async def demo_anthropic_provider():
|
| 17 |
+
"""Demo using LLMClient with Anthropic provider."""
|
| 18 |
+
print("\n" + "=" * 60)
|
| 19 |
+
print("DEMO: LLMClient with Anthropic Provider")
|
| 20 |
+
print("=" * 60)
|
| 21 |
+
|
| 22 |
+
# Load config
|
| 23 |
+
config_path = Path("mini_agent/config/config.yaml")
|
| 24 |
+
with open(config_path, encoding="utf-8") as f:
|
| 25 |
+
config = yaml.safe_load(f)
|
| 26 |
+
|
| 27 |
+
# Initialize client with Anthropic provider
|
| 28 |
+
client = LLMClient(
|
| 29 |
+
api_key=config["api_key"],
|
| 30 |
+
provider=LLMProvider.ANTHROPIC, # Specify Anthropic provider
|
| 31 |
+
model=config.get("model", "MiniMax-M2.1"),
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
print(f"Provider: {client.provider}")
|
| 35 |
+
print(f"API Base: {client.api_base}")
|
| 36 |
+
|
| 37 |
+
# Simple question
|
| 38 |
+
messages = [Message(role="user", content="Say 'Hello from Anthropic!'")]
|
| 39 |
+
print(f"\n👤 User: {messages[0].content}")
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
response = await client.generate(messages)
|
| 43 |
+
if response.thinking:
|
| 44 |
+
print(f"💭 Thinking: {response.thinking}")
|
| 45 |
+
print(f"💬 Model: {response.content}")
|
| 46 |
+
print("✅ Anthropic provider demo completed")
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"❌ Error: {e}")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
async def demo_openai_provider():
|
| 52 |
+
"""Demo using LLMClient with OpenAI provider."""
|
| 53 |
+
print("\n" + "=" * 60)
|
| 54 |
+
print("DEMO: LLMClient with OpenAI Provider")
|
| 55 |
+
print("=" * 60)
|
| 56 |
+
|
| 57 |
+
# Load config
|
| 58 |
+
config_path = Path("mini_agent/config/config.yaml")
|
| 59 |
+
with open(config_path, encoding="utf-8") as f:
|
| 60 |
+
config = yaml.safe_load(f)
|
| 61 |
+
|
| 62 |
+
# Initialize client with OpenAI provider
|
| 63 |
+
client = LLMClient(
|
| 64 |
+
api_key=config["api_key"],
|
| 65 |
+
provider=LLMProvider.OPENAI, # Specify OpenAI provider
|
| 66 |
+
model=config.get("model", "MiniMax-M2.1"),
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
print(f"Provider: {client.provider}")
|
| 70 |
+
print(f"API Base: {client.api_base}")
|
| 71 |
+
|
| 72 |
+
# Simple question
|
| 73 |
+
messages = [Message(role="user", content="Say 'Hello from OpenAI!'")]
|
| 74 |
+
print(f"\n👤 User: {messages[0].content}")
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
response = await client.generate(messages)
|
| 78 |
+
if response.thinking:
|
| 79 |
+
print(f"💭 Thinking: {response.thinking}")
|
| 80 |
+
print(f"💬 Model: {response.content}")
|
| 81 |
+
print("✅ OpenAI provider demo completed")
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"❌ Error: {e}")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
async def demo_default_provider():
|
| 87 |
+
"""Demo using LLMClient with default provider."""
|
| 88 |
+
print("\n" + "=" * 60)
|
| 89 |
+
print("DEMO: LLMClient with Default Provider (Anthropic)")
|
| 90 |
+
print("=" * 60)
|
| 91 |
+
|
| 92 |
+
# Load config
|
| 93 |
+
config_path = Path("mini_agent/config/config.yaml")
|
| 94 |
+
with open(config_path, encoding="utf-8") as f:
|
| 95 |
+
config = yaml.safe_load(f)
|
| 96 |
+
|
| 97 |
+
# Initialize client without specifying provider (defaults to Anthropic)
|
| 98 |
+
client = LLMClient(
|
| 99 |
+
api_key=config["api_key"],
|
| 100 |
+
model=config.get("model", "MiniMax-M2.1"),
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
print(f"Provider (default): {client.provider}")
|
| 104 |
+
print(f"API Base: {client.api_base}")
|
| 105 |
+
|
| 106 |
+
# Simple question
|
| 107 |
+
messages = [Message(role="user", content="Say 'Hello with default provider!'")]
|
| 108 |
+
print(f"\n👤 User: {messages[0].content}")
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
response = await client.generate(messages)
|
| 112 |
+
print(f"💬 Model: {response.content}")
|
| 113 |
+
print("✅ Default provider demo completed")
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print(f"❌ Error: {e}")
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
async def demo_provider_comparison():
|
| 119 |
+
"""Compare responses from both providers."""
|
| 120 |
+
print("\n" + "=" * 60)
|
| 121 |
+
print("DEMO: Provider Comparison")
|
| 122 |
+
print("=" * 60)
|
| 123 |
+
|
| 124 |
+
# Load config
|
| 125 |
+
config_path = Path("mini_agent/config/config.yaml")
|
| 126 |
+
with open(config_path, encoding="utf-8") as f:
|
| 127 |
+
config = yaml.safe_load(f)
|
| 128 |
+
|
| 129 |
+
# Create clients for both providers
|
| 130 |
+
anthropic_client = LLMClient(
|
| 131 |
+
api_key=config["api_key"],
|
| 132 |
+
provider=LLMProvider.ANTHROPIC,
|
| 133 |
+
model=config.get("model", "MiniMax-M2.1"),
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
openai_client = LLMClient(
|
| 137 |
+
api_key=config["api_key"],
|
| 138 |
+
provider=LLMProvider.OPENAI,
|
| 139 |
+
model=config.get("model", "MiniMax-M2.1"),
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
# Same question for both
|
| 143 |
+
messages = [Message(role="user", content="What is 2+2?")]
|
| 144 |
+
print(f"\n👤 Question: {messages[0].content}\n")
|
| 145 |
+
|
| 146 |
+
try:
|
| 147 |
+
# Get response from Anthropic
|
| 148 |
+
anthropic_response = await anthropic_client.generate(messages)
|
| 149 |
+
print(f"🔵 Anthropic: {anthropic_response.content}")
|
| 150 |
+
|
| 151 |
+
# Get response from OpenAI
|
| 152 |
+
openai_response = await openai_client.generate(messages)
|
| 153 |
+
print(f"🟢 OpenAI: {openai_response.content}")
|
| 154 |
+
|
| 155 |
+
print("\n✅ Provider comparison completed")
|
| 156 |
+
except Exception as e:
|
| 157 |
+
print(f"❌ Error: {e}")
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
async def main():
|
| 161 |
+
"""Run all demos."""
|
| 162 |
+
print("\n🚀 LLM Provider Selection Demo")
|
| 163 |
+
print("This demo shows how to use LLMClient with different providers.")
|
| 164 |
+
print("Make sure you have configured API key in config.yaml.")
|
| 165 |
+
|
| 166 |
+
try:
|
| 167 |
+
# Demo default provider
|
| 168 |
+
await demo_default_provider()
|
| 169 |
+
|
| 170 |
+
# Demo Anthropic provider
|
| 171 |
+
await demo_anthropic_provider()
|
| 172 |
+
|
| 173 |
+
# Demo OpenAI provider
|
| 174 |
+
await demo_openai_provider()
|
| 175 |
+
|
| 176 |
+
# Demo provider comparison
|
| 177 |
+
await demo_provider_comparison()
|
| 178 |
+
|
| 179 |
+
print("\n✅ All demos completed successfully!")
|
| 180 |
+
|
| 181 |
+
except Exception as e:
|
| 182 |
+
print(f"\n❌ Error: {e}")
|
| 183 |
+
import traceback
|
| 184 |
+
|
| 185 |
+
traceback.print_exc()
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
if __name__ == "__main__":
|
| 189 |
+
asyncio.run(main())
|
| 190 |
+
|
examples/06_tool_schema_demo.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Demo: Using Tool schemas with base Tool class.
|
| 2 |
+
|
| 3 |
+
This example demonstrates how to use the Tool base class and its schema methods.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
import yaml
|
| 11 |
+
|
| 12 |
+
from mini_agent import LLMClient, LLMProvider
|
| 13 |
+
from mini_agent.schema import Message
|
| 14 |
+
from mini_agent.tools.base import Tool, ToolResult
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load_config():
|
| 18 |
+
"""Load config from config.yaml."""
|
| 19 |
+
config_path = Path("mini_agent/config/config.yaml")
|
| 20 |
+
with open(config_path, encoding="utf-8") as f:
|
| 21 |
+
return yaml.safe_load(f)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class WeatherTool(Tool):
|
| 25 |
+
"""Example weather tool."""
|
| 26 |
+
|
| 27 |
+
@property
|
| 28 |
+
def name(self) -> str:
|
| 29 |
+
return "get_weather"
|
| 30 |
+
|
| 31 |
+
@property
|
| 32 |
+
def description(self) -> str:
|
| 33 |
+
return "Get current weather information for a location. Returns temperature and conditions."
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def parameters(self) -> dict[str, Any]:
|
| 37 |
+
return {
|
| 38 |
+
"type": "object",
|
| 39 |
+
"properties": {
|
| 40 |
+
"location": {
|
| 41 |
+
"type": "string",
|
| 42 |
+
"description": "City and state, e.g. 'San Francisco, CA' or 'London, UK'",
|
| 43 |
+
},
|
| 44 |
+
"unit": {
|
| 45 |
+
"type": "string",
|
| 46 |
+
"enum": ["celsius", "fahrenheit"],
|
| 47 |
+
"description": "Temperature unit (celsius or fahrenheit)",
|
| 48 |
+
},
|
| 49 |
+
},
|
| 50 |
+
"required": ["location"],
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
async def execute(self, **kwargs) -> ToolResult:
|
| 54 |
+
"""Mock execute method."""
|
| 55 |
+
return ToolResult(success=True, content="Weather data")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class SearchTool(Tool):
|
| 59 |
+
"""Example search tool."""
|
| 60 |
+
|
| 61 |
+
@property
|
| 62 |
+
def name(self) -> str:
|
| 63 |
+
return "search_web"
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def description(self) -> str:
|
| 67 |
+
return "Search the web for information about a topic"
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def parameters(self) -> dict[str, Any]:
|
| 71 |
+
return {
|
| 72 |
+
"type": "object",
|
| 73 |
+
"properties": {
|
| 74 |
+
"query": {
|
| 75 |
+
"type": "string",
|
| 76 |
+
"description": "Search query string",
|
| 77 |
+
},
|
| 78 |
+
"max_results": {
|
| 79 |
+
"type": "integer",
|
| 80 |
+
"description": "Maximum number of results to return (1-10)",
|
| 81 |
+
},
|
| 82 |
+
},
|
| 83 |
+
"required": ["query"],
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
async def execute(self, **kwargs) -> ToolResult:
|
| 87 |
+
"""Mock execute method."""
|
| 88 |
+
return ToolResult(success=True, content="Search results")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class CalculatorTool(Tool):
|
| 92 |
+
"""Example calculator tool."""
|
| 93 |
+
|
| 94 |
+
@property
|
| 95 |
+
def name(self) -> str:
|
| 96 |
+
return "calculator"
|
| 97 |
+
|
| 98 |
+
@property
|
| 99 |
+
def description(self) -> str:
|
| 100 |
+
return "Perform arithmetic calculations"
|
| 101 |
+
|
| 102 |
+
@property
|
| 103 |
+
def parameters(self) -> dict[str, Any]:
|
| 104 |
+
return {
|
| 105 |
+
"type": "object",
|
| 106 |
+
"properties": {
|
| 107 |
+
"expression": {
|
| 108 |
+
"type": "string",
|
| 109 |
+
"description": "Mathematical expression to evaluate, e.g. '2 + 2' or '10 * 5'",
|
| 110 |
+
}
|
| 111 |
+
},
|
| 112 |
+
"required": ["expression"],
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
async def execute(self, **kwargs) -> ToolResult:
|
| 116 |
+
"""Mock execute method."""
|
| 117 |
+
return ToolResult(success=True, content="Calculation result")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class TranslateTool(Tool):
|
| 121 |
+
"""Example translate tool."""
|
| 122 |
+
|
| 123 |
+
@property
|
| 124 |
+
def name(self) -> str:
|
| 125 |
+
return "translate"
|
| 126 |
+
|
| 127 |
+
@property
|
| 128 |
+
def description(self) -> str:
|
| 129 |
+
return "Translate text from one language to another"
|
| 130 |
+
|
| 131 |
+
@property
|
| 132 |
+
def parameters(self) -> dict[str, Any]:
|
| 133 |
+
return {
|
| 134 |
+
"type": "object",
|
| 135 |
+
"properties": {
|
| 136 |
+
"text": {
|
| 137 |
+
"type": "string",
|
| 138 |
+
"description": "Text to translate",
|
| 139 |
+
},
|
| 140 |
+
"target_language": {
|
| 141 |
+
"type": "string",
|
| 142 |
+
"description": "Target language code (e.g. 'en', 'es', 'fr')",
|
| 143 |
+
},
|
| 144 |
+
},
|
| 145 |
+
"required": ["text", "target_language"],
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
async def execute(self, **kwargs) -> ToolResult:
|
| 149 |
+
"""Mock execute method."""
|
| 150 |
+
return ToolResult(success=True, content="Translation result")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
async def demo_tool_schemas():
|
| 154 |
+
"""Demonstrate using Tool objects with LLM."""
|
| 155 |
+
config = load_config()
|
| 156 |
+
|
| 157 |
+
print("=" * 60)
|
| 158 |
+
print("Method 1: Using Tool Objects with LLM")
|
| 159 |
+
print("=" * 60)
|
| 160 |
+
|
| 161 |
+
# Create tool instances
|
| 162 |
+
weather_tool = WeatherTool()
|
| 163 |
+
search_tool = SearchTool()
|
| 164 |
+
|
| 165 |
+
# Create client
|
| 166 |
+
client = LLMClient(
|
| 167 |
+
api_key=config["api_key"],
|
| 168 |
+
provider=LLMProvider.ANTHROPIC,
|
| 169 |
+
model="MiniMax-M2.1",
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
# Test with a query that should trigger weather tool
|
| 173 |
+
messages = [
|
| 174 |
+
Message(
|
| 175 |
+
role="user",
|
| 176 |
+
content="What's the weather like in Tokyo? I want it in celsius.",
|
| 177 |
+
)
|
| 178 |
+
]
|
| 179 |
+
|
| 180 |
+
print("\nQuery: What's the weather like in Tokyo? I want it in celsius.")
|
| 181 |
+
print("\nAvailable tools:")
|
| 182 |
+
print(f" 1. {weather_tool.name}: {weather_tool.description}")
|
| 183 |
+
print(f" 2. {search_tool.name}: {search_tool.description}")
|
| 184 |
+
|
| 185 |
+
# Pass Tool objects directly to generate
|
| 186 |
+
response = await client.generate(
|
| 187 |
+
messages,
|
| 188 |
+
tools=[weather_tool, search_tool], # Using Tool objects
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
print(f"\nResponse content: {response.content}")
|
| 192 |
+
|
| 193 |
+
if response.thinking:
|
| 194 |
+
print(f"\nThinking: {response.thinking}")
|
| 195 |
+
|
| 196 |
+
if response.tool_calls:
|
| 197 |
+
print(f"\nTool calls made: {len(response.tool_calls)}")
|
| 198 |
+
for tool_call in response.tool_calls:
|
| 199 |
+
print(f" - Function: {tool_call.function.name}")
|
| 200 |
+
print(f" Arguments: {tool_call.function.arguments}")
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
async def demo_multiple_tools():
|
| 204 |
+
"""Demonstrate using multiple Tool instances."""
|
| 205 |
+
config = load_config()
|
| 206 |
+
|
| 207 |
+
print("\n" + "=" * 60)
|
| 208 |
+
print("Method 2: Using Multiple Tool Instances")
|
| 209 |
+
print("=" * 60)
|
| 210 |
+
|
| 211 |
+
# Create tool instances
|
| 212 |
+
calculator_tool = CalculatorTool()
|
| 213 |
+
translate_tool = TranslateTool()
|
| 214 |
+
|
| 215 |
+
client = LLMClient(
|
| 216 |
+
api_key=config["api_key"],
|
| 217 |
+
provider=LLMProvider.ANTHROPIC,
|
| 218 |
+
model="MiniMax-M2.1",
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
messages = [Message(role="user", content="Calculate 15 * 23 for me")]
|
| 222 |
+
|
| 223 |
+
print("\nQuery: Calculate 15 * 23 for me")
|
| 224 |
+
print("\nAvailable tools:")
|
| 225 |
+
print(" 1. calculator (Tool)")
|
| 226 |
+
print(" 2. translate (Tool)")
|
| 227 |
+
|
| 228 |
+
response = await client.generate(messages, tools=[calculator_tool, translate_tool])
|
| 229 |
+
|
| 230 |
+
print(f"\nResponse content: {response.content}")
|
| 231 |
+
|
| 232 |
+
if response.thinking:
|
| 233 |
+
print(f"\nThinking: {response.thinking}")
|
| 234 |
+
|
| 235 |
+
if response.tool_calls:
|
| 236 |
+
print(f"\nTool calls made: {len(response.tool_calls)}")
|
| 237 |
+
for tool_call in response.tool_calls:
|
| 238 |
+
print(f" - Function: {tool_call.function.name}")
|
| 239 |
+
print(f" Arguments: {tool_call.function.arguments}")
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
async def demo_tool_schema_methods():
|
| 243 |
+
"""Demonstrate Tool schema conversion methods."""
|
| 244 |
+
print("\n" + "=" * 60)
|
| 245 |
+
print("Method 3: Tool Schema Conversion Methods")
|
| 246 |
+
print("=" * 60)
|
| 247 |
+
|
| 248 |
+
weather_tool = WeatherTool()
|
| 249 |
+
|
| 250 |
+
print("\nTool to Anthropic schema (to_schema):")
|
| 251 |
+
anthropic_schema = weather_tool.to_schema()
|
| 252 |
+
print(f" {anthropic_schema}")
|
| 253 |
+
|
| 254 |
+
print("\nTool to OpenAI schema (to_openai_schema):")
|
| 255 |
+
openai_schema = weather_tool.to_openai_schema()
|
| 256 |
+
print(f" {openai_schema}")
|
| 257 |
+
|
| 258 |
+
print("\nSchema methods allow flexible tool usage with different LLM providers.")
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
async def main():
|
| 262 |
+
"""Run all demos."""
|
| 263 |
+
print("\n🚀 Tool Schema Demo - Using Tool Base Class\n")
|
| 264 |
+
|
| 265 |
+
try:
|
| 266 |
+
# Demo 1: Tool objects with LLM
|
| 267 |
+
await demo_tool_schemas()
|
| 268 |
+
|
| 269 |
+
# Demo 2: Multiple tools
|
| 270 |
+
await demo_multiple_tools()
|
| 271 |
+
|
| 272 |
+
# Demo 3: Schema methods
|
| 273 |
+
await demo_tool_schema_methods()
|
| 274 |
+
|
| 275 |
+
print("\n✅ All demos completed successfully!")
|
| 276 |
+
|
| 277 |
+
except Exception as e:
|
| 278 |
+
print(f"\n❌ Error: {e}")
|
| 279 |
+
import traceback
|
| 280 |
+
|
| 281 |
+
traceback.print_exc()
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
if __name__ == "__main__":
|
| 285 |
+
asyncio.run(main())
|
examples/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Mini Agent Examples
|
| 2 |
+
|
| 3 |
+
This directory contains a series of progressive examples to help you understand how to use the Mini Agent framework.
|
| 4 |
+
|
| 5 |
+
## 📚 Example List
|
| 6 |
+
|
| 7 |
+
### 01_basic_tools.py - Basic Tool Usage
|
| 8 |
+
|
| 9 |
+
**Difficulty**: ⭐ Beginner
|
| 10 |
+
|
| 11 |
+
**Content**:
|
| 12 |
+
- How to directly use ReadTool, WriteTool, EditTool, BashTool
|
| 13 |
+
- No Agent or LLM involved, pure tool call demonstrations
|
| 14 |
+
- Perfect for understanding each tool's basic functionality
|
| 15 |
+
|
| 16 |
+
**Run**:
|
| 17 |
+
```bash
|
| 18 |
+
python examples/01_basic_tools.py
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
**Key Learnings**:
|
| 22 |
+
- Tool input parameter formats
|
| 23 |
+
- ToolResult return structure
|
| 24 |
+
- Error handling approaches
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
### 02_simple_agent.py - Simple Agent Usage
|
| 29 |
+
|
| 30 |
+
**Difficulty**: ⭐⭐ Beginner-Intermediate
|
| 31 |
+
|
| 32 |
+
**Content**:
|
| 33 |
+
- Create the simplest Agent
|
| 34 |
+
- Have Agent perform file creation tasks
|
| 35 |
+
- Have Agent execute bash command tasks
|
| 36 |
+
- Understand Agent execution flow
|
| 37 |
+
|
| 38 |
+
**Run**:
|
| 39 |
+
```bash
|
| 40 |
+
# Requires API key configuration first
|
| 41 |
+
python examples/02_simple_agent.py
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
**Key Learnings**:
|
| 45 |
+
- Agent initialization process
|
| 46 |
+
- How to give tasks to Agent
|
| 47 |
+
- How Agent autonomously selects tools
|
| 48 |
+
- Task completion criteria
|
| 49 |
+
|
| 50 |
+
**Prerequisites**:
|
| 51 |
+
- API key configured in `mini_agent/config/config.yaml`
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
### 03_session_notes.py - Session Note Tool
|
| 56 |
+
|
| 57 |
+
**Difficulty**: ⭐⭐⭐ Intermediate
|
| 58 |
+
|
| 59 |
+
**Content**:
|
| 60 |
+
- Direct usage of Session Note tools (record_note, recall_notes)
|
| 61 |
+
- Agent using Session Notes to maintain cross-session memory
|
| 62 |
+
- Demonstrate how two Agent instances share memory
|
| 63 |
+
|
| 64 |
+
**Run**:
|
| 65 |
+
```bash
|
| 66 |
+
python examples/03_session_notes.py
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
**Key Learnings**:
|
| 70 |
+
- How Session Notes work
|
| 71 |
+
- Note categorization management (category)
|
| 72 |
+
- How to guide Agent to use notes in system prompt
|
| 73 |
+
- Cross-session memory implementation
|
| 74 |
+
|
| 75 |
+
**Highlight**:
|
| 76 |
+
This is one of the core features of this project! Shows a lightweight but effective session memory management solution.
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
### 04_full_agent.py - Full-Featured Agent
|
| 81 |
+
|
| 82 |
+
**Difficulty**: ⭐⭐⭐⭐ Advanced
|
| 83 |
+
|
| 84 |
+
**Content**:
|
| 85 |
+
- Complete Agent setup with all features
|
| 86 |
+
- Integration of basic tools + Session Notes + MCP tools
|
| 87 |
+
- Full execution flow for complex tasks
|
| 88 |
+
- Multi-turn conversation examples
|
| 89 |
+
|
| 90 |
+
**Run**:
|
| 91 |
+
```bash
|
| 92 |
+
python examples/04_full_agent.py
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
**Key Learnings**:
|
| 96 |
+
- How to combine multiple tools
|
| 97 |
+
- MCP tool loading and usage
|
| 98 |
+
- Complex task decomposition and execution
|
| 99 |
+
- Production environment Agent configuration
|
| 100 |
+
|
| 101 |
+
**Prerequisites**:
|
| 102 |
+
- API key configured
|
| 103 |
+
- (Optional) MCP tools configured
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
## 🚀 Quick Start
|
| 108 |
+
|
| 109 |
+
### 1. Configure API Key
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
# Copy configuration template
|
| 113 |
+
cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
|
| 114 |
+
|
| 115 |
+
# Edit config file and fill in your MiniMax API Key
|
| 116 |
+
vim mini_agent/config/config.yaml
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
### 2. Run Your First Example
|
| 120 |
+
|
| 121 |
+
```bash
|
| 122 |
+
# Example that doesn't need API key
|
| 123 |
+
python examples/01_basic_tools.py
|
| 124 |
+
|
| 125 |
+
# Example that needs API key
|
| 126 |
+
python examples/02_simple_agent.py
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
### 3. Progressive Learning
|
| 130 |
+
|
| 131 |
+
Recommended to learn in numerical order:
|
| 132 |
+
1. **01_basic_tools.py** - Understand tools
|
| 133 |
+
2. **02_simple_agent.py** - Understand Agent
|
| 134 |
+
3. **03_session_notes.py** - Understand memory management
|
| 135 |
+
4. **04_full_agent.py** - Understand complete system
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
## 📖 Relationship with Test Cases
|
| 140 |
+
|
| 141 |
+
These examples are all refined from test cases in the `tests/` directory:
|
| 142 |
+
|
| 143 |
+
| Example | Based on Test | Description |
|
| 144 |
+
| ------------------- | ---------------------------------------------------- | ------------------------------- |
|
| 145 |
+
| 01_basic_tools.py | tests/test_tools.py | Basic tool unit tests |
|
| 146 |
+
| 02_simple_agent.py | tests/test_agent.py | Agent basic functionality tests |
|
| 147 |
+
| 03_session_notes.py | tests/test_note_tool.py<br>tests/test_integration.py | Session Note tool tests |
|
| 148 |
+
| 04_full_agent.py | tests/test_integration.py | Complete integration tests |
|
| 149 |
+
|
| 150 |
+
---
|
| 151 |
+
|
| 152 |
+
## 💡 Recommended Learning Paths
|
| 153 |
+
|
| 154 |
+
### Path 1: Quick Start
|
| 155 |
+
1. Run `01_basic_tools.py` - Learn about tools
|
| 156 |
+
2. Run `02_simple_agent.py` - Run your first Agent
|
| 157 |
+
3. Go directly to interactive mode with `mini-agent`
|
| 158 |
+
|
| 159 |
+
### Path 2: Deep Understanding
|
| 160 |
+
1. Read and run all examples (01 → 04)
|
| 161 |
+
2. Read corresponding test cases (`tests/`)
|
| 162 |
+
3. Read core implementation code (`mini_agent/`)
|
| 163 |
+
4. Try modifying examples to implement your own features
|
| 164 |
+
|
| 165 |
+
### Path 3: Production Application
|
| 166 |
+
1. Understand all examples
|
| 167 |
+
2. Read [Production Deployment Guide](../docs/PRODUCTION_GUIDE.md)
|
| 168 |
+
3. Configure MCP tools and Skills
|
| 169 |
+
4. Extend tool set based on needs
|
| 170 |
+
|
| 171 |
+
---
|
| 172 |
+
|
| 173 |
+
## 🔧 Troubleshooting
|
| 174 |
+
|
| 175 |
+
### API Key Error
|
| 176 |
+
```
|
| 177 |
+
❌ API key not configured in config.yaml
|
| 178 |
+
```
|
| 179 |
+
**Solution**: Ensure you've configured a valid MiniMax API Key in `mini_agent/config/config.yaml`
|
| 180 |
+
|
| 181 |
+
### config.yaml Not Found
|
| 182 |
+
```
|
| 183 |
+
❌ config.yaml not found
|
| 184 |
+
```
|
| 185 |
+
**Solution**:
|
| 186 |
+
```bash
|
| 187 |
+
cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
### MCP Tools Loading Failed
|
| 191 |
+
```
|
| 192 |
+
⚠️ MCP tools not loaded: [error message]
|
| 193 |
+
```
|
| 194 |
+
**Solution**: MCP tools are optional and don't affect basic functionality. If you need them, refer to the MCP configuration section in the main README.
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## 📚 More Resources
|
| 199 |
+
|
| 200 |
+
- [Main Project README](../README.md) - Complete project documentation
|
| 201 |
+
- [Test Cases](../tests/) - More usage examples
|
| 202 |
+
- [Core Implementation](../mini_agent/) - Source code
|
| 203 |
+
- [Production Guide](../docs/PRODUCTION_GUIDE.md) - Deployment guide
|
| 204 |
+
|
| 205 |
+
---
|
| 206 |
+
|
| 207 |
+
## 🤝 Contributing Examples
|
| 208 |
+
|
| 209 |
+
If you have good usage examples, PRs are welcome!
|
| 210 |
+
|
| 211 |
+
Suggested new example directions:
|
| 212 |
+
- Web search integration examples (using MiniMax Search MCP)
|
| 213 |
+
- Skills usage examples (document processing, design, etc.)
|
| 214 |
+
- Custom tool development examples
|
| 215 |
+
- Error handling and retry mechanism examples
|
| 216 |
+
|
| 217 |
+
---
|
| 218 |
+
|
| 219 |
+
**⭐ If these examples help you, please give the project a Star!**
|
examples/README_CN.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Mini Agent Examples
|
| 2 |
+
|
| 3 |
+
这个目录包含了一系列渐进式的示例,帮助你理解如何使用 Mini Agent 框架。
|
| 4 |
+
|
| 5 |
+
## 📚 示例列表
|
| 6 |
+
|
| 7 |
+
### 01_basic_tools.py - 基础工具使用
|
| 8 |
+
|
| 9 |
+
**难度**: ⭐ 入门
|
| 10 |
+
|
| 11 |
+
**内容**:
|
| 12 |
+
- 如何直接使用 ReadTool、WriteTool、EditTool、BashTool
|
| 13 |
+
- 不涉及 Agent 或 LLM,纯粹的工具调用演示
|
| 14 |
+
- 适合理解每个工具的基本功能
|
| 15 |
+
|
| 16 |
+
**运行**:
|
| 17 |
+
```bash
|
| 18 |
+
python examples/01_basic_tools.py
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
**学习要点**:
|
| 22 |
+
- 工具的输入参数格式
|
| 23 |
+
- ToolResult 的返回结构
|
| 24 |
+
- 错误处理方式
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
### 02_simple_agent.py - 简单 Agent 使用
|
| 29 |
+
|
| 30 |
+
**难度**: ⭐⭐ 初级
|
| 31 |
+
|
| 32 |
+
**内容**:
|
| 33 |
+
- 创建最简单的 Agent
|
| 34 |
+
- 让 Agent 执行文件创建任务
|
| 35 |
+
- 让 Agent 执行 bash 命令任务
|
| 36 |
+
- 理解 Agent 的执行流程
|
| 37 |
+
|
| 38 |
+
**运行**:
|
| 39 |
+
```bash
|
| 40 |
+
# 需要先配置 API key
|
| 41 |
+
python examples/02_simple_agent.py
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
**学习要点**:
|
| 45 |
+
- Agent 的初始化流程
|
| 46 |
+
- 如何给 Agent 下达任务
|
| 47 |
+
- Agent 如何自主选择工具
|
| 48 |
+
- 任务完成的判断标准
|
| 49 |
+
|
| 50 |
+
**前置要求**:
|
| 51 |
+
- 已配置 `mini_agent/config/config.yaml` 中的 API key
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
### 03_session_notes.py - Session Note 工具
|
| 56 |
+
|
| 57 |
+
**难度**: ⭐⭐⭐ 中级
|
| 58 |
+
|
| 59 |
+
**内容**:
|
| 60 |
+
- 直接使用 Session Note 工具(record_note, recall_notes)
|
| 61 |
+
- Agent 使用 Session Note 保持跨会话记忆
|
| 62 |
+
- 演示两个 Agent 实例如何共享记忆
|
| 63 |
+
|
| 64 |
+
**运行**:
|
| 65 |
+
```bash
|
| 66 |
+
python examples/03_session_notes.py
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
**学习要点**:
|
| 70 |
+
- Session Note 的工作原理
|
| 71 |
+
- 笔记的分类管理(category)
|
| 72 |
+
- 如何在 system prompt 中引导 Agent 使用笔记
|
| 73 |
+
- 跨会话记忆的实现方式
|
| 74 |
+
|
| 75 |
+
**亮点**:
|
| 76 |
+
这是本项目的核心特性之一!展示了一种轻量但有效的会话记忆管理方案。
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
### 04_full_agent.py - 完整功能 Agent
|
| 81 |
+
|
| 82 |
+
**难度**: ⭐⭐⭐⭐ 高级
|
| 83 |
+
|
| 84 |
+
**内容**:
|
| 85 |
+
- 包含所有功能的完整 Agent 设置
|
| 86 |
+
- 集成基础工具 + Session Notes + MCP 工具
|
| 87 |
+
- 复杂任务的完整执行流程
|
| 88 |
+
- 多轮对话示例
|
| 89 |
+
|
| 90 |
+
**运行**:
|
| 91 |
+
```bash
|
| 92 |
+
python examples/04_full_agent.py
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
**学习要点**:
|
| 96 |
+
- 如何组合多种工具
|
| 97 |
+
- MCP 工具的加载和使用
|
| 98 |
+
- 复杂任务的分解和执行
|
| 99 |
+
- 生产环境的 Agent 配置
|
| 100 |
+
|
| 101 |
+
**前置要求**:
|
| 102 |
+
- 已配置 API key
|
| 103 |
+
- (可选)配置了 MCP 工具
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
## 🚀 快速开始
|
| 108 |
+
|
| 109 |
+
### 1. 配置 API Key
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
# 复制配置模板
|
| 113 |
+
cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
|
| 114 |
+
|
| 115 |
+
# 编辑配置文件,填入你的 MiniMax API Key
|
| 116 |
+
vim mini_agent/config/config.yaml
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
### 2. 运行第一个示例
|
| 120 |
+
|
| 121 |
+
```bash
|
| 122 |
+
# 不需要 API key 的示例
|
| 123 |
+
python examples/01_basic_tools.py
|
| 124 |
+
|
| 125 |
+
# 需要 API key 的示例
|
| 126 |
+
python examples/02_simple_agent.py
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
### 3. 逐步学习
|
| 130 |
+
|
| 131 |
+
建议按照编号顺序学习:
|
| 132 |
+
1. **01_basic_tools.py** - 理解工具
|
| 133 |
+
2. **02_simple_agent.py** - 理解 Agent
|
| 134 |
+
3. **03_session_notes.py** - 理解记忆管理
|
| 135 |
+
4. **04_full_agent.py** - 理解完整系统
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
## 📖 与测试用例的对应关系
|
| 140 |
+
|
| 141 |
+
这些示例都是基于 `tests/` 目录中的测试用例提炼而来:
|
| 142 |
+
|
| 143 |
+
| Example | Based on Test | Description |
|
| 144 |
+
| ------------------- | ---------------------------------------------------- | --------------------- |
|
| 145 |
+
| 01_basic_tools.py | tests/test_tools.py | 基础工具单元测试 |
|
| 146 |
+
| 02_simple_agent.py | tests/test_agent.py | Agent 基本功能测试 |
|
| 147 |
+
| 03_session_notes.py | tests/test_note_tool.py<br>tests/test_integration.py | Session Note 工具测试 |
|
| 148 |
+
| 04_full_agent.py | tests/test_integration.py | 完整集成测试 |
|
| 149 |
+
|
| 150 |
+
---
|
| 151 |
+
|
| 152 |
+
## 💡 学习路径建议
|
| 153 |
+
|
| 154 |
+
### 路径 1: 快速上手
|
| 155 |
+
1. 运行 `01_basic_tools.py` - 了解工具
|
| 156 |
+
2. 运行 `02_simple_agent.py` - 运行第一个 Agent
|
| 157 |
+
3. 直接使用 `mini-agent` 进入交互模式
|
| 158 |
+
|
| 159 |
+
### 路径 2: 深入理解
|
| 160 |
+
1. 阅读并运行所有示例 (01 → 04)
|
| 161 |
+
2. 阅读对应的测试用例 (`tests/`)
|
| 162 |
+
3. 阅读核心实现代码 (`mini_agent/`)
|
| 163 |
+
4. 尝试修改示例,实现自己的功能
|
| 164 |
+
|
| 165 |
+
### 路径 3: 生产应用
|
| 166 |
+
1. 理解所有示例
|
| 167 |
+
2. 阅读 [生产环境部署指南](../docs/PRODUCTION_GUIDE.md)
|
| 168 |
+
3. 配置 MCP 工具和 Skills
|
| 169 |
+
4. 根据需求扩展工具集
|
| 170 |
+
|
| 171 |
+
---
|
| 172 |
+
|
| 173 |
+
## 🔧 故障排除
|
| 174 |
+
|
| 175 |
+
### API Key 错误
|
| 176 |
+
```
|
| 177 |
+
❌ API key not configured in config.yaml
|
| 178 |
+
```
|
| 179 |
+
**解决**: 确保在 `mini_agent/config/config.yaml` 中配置了有效的 MiniMax API Key
|
| 180 |
+
|
| 181 |
+
### 找不到 config.yaml
|
| 182 |
+
```
|
| 183 |
+
❌ config.yaml not found
|
| 184 |
+
```
|
| 185 |
+
**解决**:
|
| 186 |
+
```bash
|
| 187 |
+
cp mini_agent/config/config-example.yaml mini_agent/config/config.yaml
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
### MCP 工具加载失败
|
| 191 |
+
```
|
| 192 |
+
⚠️ MCP tools not loaded: [error message]
|
| 193 |
+
```
|
| 194 |
+
**解决**: MCP 工具是可选的,不影响基本功能。如需使用,请参考主 README 中的 MCP 配置章节。
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## 📚 更多资源
|
| 199 |
+
|
| 200 |
+
- [项目主 README](../README.md) - 完整项目文档
|
| 201 |
+
- [测试用例](../tests/) - 更多使用示例
|
| 202 |
+
- [核心实现](../mini_agent/) - 源代码
|
| 203 |
+
- [生产环境指南](../docs/PRODUCTION_GUIDE.md) - 部署指南
|
| 204 |
+
|
| 205 |
+
---
|
| 206 |
+
|
| 207 |
+
## 🤝 贡献示例
|
| 208 |
+
|
| 209 |
+
如果你有好的使用示例,欢迎提交 PR!
|
| 210 |
+
|
| 211 |
+
建议的新示例方向:
|
| 212 |
+
- Web 搜索集成示例(使用 MiniMax Search MCP)
|
| 213 |
+
- Skills 使用示例(文档处理、设计等)
|
| 214 |
+
- 自定义工具开发示例
|
| 215 |
+
- 错误处理和重试机制示例
|
| 216 |
+
|
| 217 |
+
---
|
| 218 |
+
|
| 219 |
+
**⭐ 如果这些示例对你有帮助,欢迎给项目一个 Star!**
|
mini_agent/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Mini Agent - Minimal single agent with basic tools and MCP support."""
|
| 2 |
+
|
| 3 |
+
from .agent import Agent
|
| 4 |
+
from .llm import LLMClient
|
| 5 |
+
from .schema import FunctionCall, LLMProvider, LLMResponse, Message, ToolCall
|
| 6 |
+
|
| 7 |
+
__version__ = "0.1.0"
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"Agent",
|
| 11 |
+
"LLMClient",
|
| 12 |
+
"LLMProvider",
|
| 13 |
+
"Message",
|
| 14 |
+
"LLMResponse",
|
| 15 |
+
"ToolCall",
|
| 16 |
+
"FunctionCall",
|
| 17 |
+
]
|
mini_agent/acp/__init__.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ACP (Agent Client Protocol) bridge for Mini-Agent."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
import logging
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
from uuid import uuid4
|
| 11 |
+
|
| 12 |
+
from acp import (
|
| 13 |
+
PROTOCOL_VERSION,
|
| 14 |
+
AgentSideConnection,
|
| 15 |
+
CancelNotification,
|
| 16 |
+
InitializeRequest,
|
| 17 |
+
InitializeResponse,
|
| 18 |
+
NewSessionRequest,
|
| 19 |
+
NewSessionResponse,
|
| 20 |
+
PromptRequest,
|
| 21 |
+
PromptResponse,
|
| 22 |
+
session_notification,
|
| 23 |
+
start_tool_call,
|
| 24 |
+
stdio_streams,
|
| 25 |
+
text_block,
|
| 26 |
+
tool_content,
|
| 27 |
+
update_agent_message,
|
| 28 |
+
update_agent_thought,
|
| 29 |
+
update_tool_call,
|
| 30 |
+
)
|
| 31 |
+
from pydantic import field_validator
|
| 32 |
+
from acp.schema import AgentCapabilities, Implementation, McpCapabilities
|
| 33 |
+
|
| 34 |
+
from mini_agent.agent import Agent
|
| 35 |
+
from mini_agent.cli import add_workspace_tools, initialize_base_tools
|
| 36 |
+
from mini_agent.config import Config
|
| 37 |
+
from mini_agent.llm import LLMClient
|
| 38 |
+
from mini_agent.retry import RetryConfig as RetryConfigBase
|
| 39 |
+
from mini_agent.schema import Message
|
| 40 |
+
|
| 41 |
+
logger = logging.getLogger(__name__)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
class InitializeRequestPatch(InitializeRequest):
|
| 46 |
+
@field_validator("protocolVersion", mode="before")
|
| 47 |
+
@classmethod
|
| 48 |
+
def normalize_protocol_version(cls, value: Any) -> int:
|
| 49 |
+
if isinstance(value, str):
|
| 50 |
+
try:
|
| 51 |
+
return int(value.split(".")[0])
|
| 52 |
+
except Exception:
|
| 53 |
+
return 1
|
| 54 |
+
if isinstance(value, (int, float)):
|
| 55 |
+
return int(value)
|
| 56 |
+
return 1
|
| 57 |
+
|
| 58 |
+
InitializeRequest = InitializeRequestPatch
|
| 59 |
+
InitializeRequest.model_rebuild(force=True)
|
| 60 |
+
except Exception: # pragma: no cover - defensive
|
| 61 |
+
logger.debug("ACP schema patch skipped")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class SessionState:
|
| 66 |
+
agent: Agent
|
| 67 |
+
cancelled: bool = False
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class MiniMaxACPAgent:
|
| 71 |
+
"""Minimal ACP adapter wrapping the existing Agent runtime."""
|
| 72 |
+
|
| 73 |
+
def __init__(
|
| 74 |
+
self,
|
| 75 |
+
conn: AgentSideConnection,
|
| 76 |
+
config: Config,
|
| 77 |
+
llm: LLMClient,
|
| 78 |
+
base_tools: list,
|
| 79 |
+
system_prompt: str,
|
| 80 |
+
):
|
| 81 |
+
self._conn = conn
|
| 82 |
+
self._config = config
|
| 83 |
+
self._llm = llm
|
| 84 |
+
self._base_tools = base_tools
|
| 85 |
+
self._system_prompt = system_prompt
|
| 86 |
+
self._sessions: dict[str, SessionState] = {}
|
| 87 |
+
|
| 88 |
+
async def initialize(self, params: InitializeRequest) -> InitializeResponse: # noqa: ARG002
|
| 89 |
+
return InitializeResponse(
|
| 90 |
+
protocolVersion=PROTOCOL_VERSION,
|
| 91 |
+
agentCapabilities=AgentCapabilities(loadSession=False),
|
| 92 |
+
agentInfo=Implementation(name="mini-agent", title="Mini-Agent", version="0.1.0"),
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
|
| 96 |
+
session_id = f"sess-{len(self._sessions)}-{uuid4().hex[:8]}"
|
| 97 |
+
workspace = Path(params.cwd or self._config.agent.workspace_dir).expanduser()
|
| 98 |
+
if not workspace.is_absolute():
|
| 99 |
+
workspace = workspace.resolve()
|
| 100 |
+
tools = list(self._base_tools)
|
| 101 |
+
add_workspace_tools(tools, self._config, workspace)
|
| 102 |
+
agent = Agent(llm_client=self._llm, system_prompt=self._system_prompt, tools=tools, max_steps=self._config.agent.max_steps, workspace_dir=str(workspace))
|
| 103 |
+
self._sessions[session_id] = SessionState(agent=agent)
|
| 104 |
+
return NewSessionResponse(sessionId=session_id)
|
| 105 |
+
|
| 106 |
+
async def prompt(self, params: PromptRequest) -> PromptResponse:
|
| 107 |
+
state = self._sessions.get(params.sessionId)
|
| 108 |
+
if not state:
|
| 109 |
+
# Auto-create session if not found (compatibility with clients that skip newSession)
|
| 110 |
+
logger.warning(f"Session '{params.sessionId}' not found, auto-creating new session")
|
| 111 |
+
new_session = await self.newSession(NewSessionRequest(cwd=None))
|
| 112 |
+
state = self._sessions.get(new_session.sessionId)
|
| 113 |
+
if not state:
|
| 114 |
+
logger.error("Failed to auto-create session")
|
| 115 |
+
return PromptResponse(stopReason="refusal")
|
| 116 |
+
state.cancelled = False
|
| 117 |
+
user_text = "\n".join(block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "") for block in params.prompt)
|
| 118 |
+
state.agent.messages.append(Message(role="user", content=user_text))
|
| 119 |
+
stop_reason = await self._run_turn(state, params.sessionId)
|
| 120 |
+
return PromptResponse(stopReason=stop_reason)
|
| 121 |
+
|
| 122 |
+
async def cancel(self, params: CancelNotification) -> None:
|
| 123 |
+
state = self._sessions.get(params.sessionId)
|
| 124 |
+
if state:
|
| 125 |
+
state.cancelled = True
|
| 126 |
+
|
| 127 |
+
async def _run_turn(self, state: SessionState, session_id: str) -> str:
|
| 128 |
+
agent = state.agent
|
| 129 |
+
for _ in range(agent.max_steps):
|
| 130 |
+
if state.cancelled:
|
| 131 |
+
return "cancelled"
|
| 132 |
+
tool_schemas = [tool.to_schema() for tool in agent.tools.values()]
|
| 133 |
+
try:
|
| 134 |
+
response = await agent.llm.generate(messages=agent.messages, tools=tool_schemas)
|
| 135 |
+
except Exception as exc:
|
| 136 |
+
logger.exception("LLM error")
|
| 137 |
+
await self._send(session_id, update_agent_message(text_block(f"Error: {exc}")))
|
| 138 |
+
return "refusal"
|
| 139 |
+
if response.thinking:
|
| 140 |
+
await self._send(session_id, update_agent_thought(text_block(response.thinking)))
|
| 141 |
+
if response.content:
|
| 142 |
+
await self._send(session_id, update_agent_message(text_block(response.content)))
|
| 143 |
+
agent.messages.append(Message(role="assistant", content=response.content, thinking=response.thinking, tool_calls=response.tool_calls))
|
| 144 |
+
if not response.tool_calls:
|
| 145 |
+
return "end_turn"
|
| 146 |
+
for call in response.tool_calls:
|
| 147 |
+
name, args = call.function.name, call.function.arguments
|
| 148 |
+
# Show tool name with key arguments for better visibility
|
| 149 |
+
args_preview = ", ".join(f"{k}={repr(v)[:50]}" for k, v in list(args.items())[:2]) if isinstance(args, dict) else ""
|
| 150 |
+
label = f"🔧 {name}({args_preview})" if args_preview else f"🔧 {name}()"
|
| 151 |
+
await self._send(session_id, start_tool_call(call.id, label, kind="execute", raw_input=args))
|
| 152 |
+
tool = agent.tools.get(name)
|
| 153 |
+
if not tool:
|
| 154 |
+
text, status = f"❌ Unknown tool: {name}", "failed"
|
| 155 |
+
else:
|
| 156 |
+
try:
|
| 157 |
+
result = await tool.execute(**args)
|
| 158 |
+
status = "completed" if result.success else "failed"
|
| 159 |
+
prefix = "✅" if result.success else "❌"
|
| 160 |
+
text = f"{prefix} {result.content if result.success else result.error or 'Tool execution failed'}"
|
| 161 |
+
except Exception as exc:
|
| 162 |
+
status, text = "failed", f"❌ Tool error: {exc}"
|
| 163 |
+
await self._send(session_id, update_tool_call(call.id, status=status, content=[tool_content(text_block(text))], raw_output=text))
|
| 164 |
+
agent.messages.append(Message(role="tool", content=text, tool_call_id=call.id, name=name))
|
| 165 |
+
return "max_turn_requests"
|
| 166 |
+
|
| 167 |
+
async def _send(self, session_id: str, update: Any) -> None:
|
| 168 |
+
await self._conn.sessionUpdate(session_notification(session_id, update))
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
async def run_acp_server(config: Config | None = None) -> None:
|
| 172 |
+
"""Run Mini-Agent as an ACP-compatible stdio server."""
|
| 173 |
+
config = config or Config.load()
|
| 174 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
| 175 |
+
base_tools, skill_loader = await initialize_base_tools(config)
|
| 176 |
+
prompt_path = Config.find_config_file(config.agent.system_prompt_path)
|
| 177 |
+
if prompt_path and prompt_path.exists():
|
| 178 |
+
system_prompt = prompt_path.read_text(encoding="utf-8")
|
| 179 |
+
else:
|
| 180 |
+
system_prompt = "You are a helpful AI assistant."
|
| 181 |
+
if skill_loader:
|
| 182 |
+
meta = skill_loader.get_skills_metadata_prompt()
|
| 183 |
+
if meta:
|
| 184 |
+
system_prompt = f"{system_prompt.rstrip()}\n\n{meta}"
|
| 185 |
+
rcfg = config.llm.retry
|
| 186 |
+
llm = LLMClient(api_key=config.llm.api_key, api_base=config.llm.api_base, model=config.llm.model, retry_config=RetryConfigBase(enabled=rcfg.enabled, max_retries=rcfg.max_retries, initial_delay=rcfg.initial_delay, max_delay=rcfg.max_delay, exponential_base=rcfg.exponential_base))
|
| 187 |
+
reader, writer = await stdio_streams()
|
| 188 |
+
AgentSideConnection(lambda conn: MiniMaxACPAgent(conn, config, llm, base_tools, system_prompt), writer, reader)
|
| 189 |
+
logger.info("Mini-Agent ACP server running")
|
| 190 |
+
await asyncio.Event().wait()
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def main() -> None:
|
| 194 |
+
asyncio.run(run_acp_server())
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
__all__ = ["MiniMaxACPAgent", "run_acp_server", "main"]
|
mini_agent/acp/server.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ACP server entry point."""
|
| 2 |
+
|
| 3 |
+
from mini_agent.acp import main
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
main()
|
mini_agent/agent.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Core Agent implementation."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from time import perf_counter
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
import tiktoken
|
| 10 |
+
|
| 11 |
+
from .llm import LLMClient
|
| 12 |
+
from .logger import AgentLogger
|
| 13 |
+
from .schema import Message
|
| 14 |
+
from .tools.base import Tool, ToolResult
|
| 15 |
+
from .utils import calculate_display_width
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# ANSI color codes
|
| 19 |
+
class Colors:
|
| 20 |
+
"""Terminal color definitions"""
|
| 21 |
+
|
| 22 |
+
RESET = "\033[0m"
|
| 23 |
+
BOLD = "\033[1m"
|
| 24 |
+
DIM = "\033[2m"
|
| 25 |
+
|
| 26 |
+
# Foreground colors
|
| 27 |
+
RED = "\033[31m"
|
| 28 |
+
GREEN = "\033[32m"
|
| 29 |
+
YELLOW = "\033[33m"
|
| 30 |
+
BLUE = "\033[34m"
|
| 31 |
+
MAGENTA = "\033[35m"
|
| 32 |
+
CYAN = "\033[36m"
|
| 33 |
+
|
| 34 |
+
# Bright colors
|
| 35 |
+
BRIGHT_BLACK = "\033[90m"
|
| 36 |
+
BRIGHT_RED = "\033[91m"
|
| 37 |
+
BRIGHT_GREEN = "\033[92m"
|
| 38 |
+
BRIGHT_YELLOW = "\033[93m"
|
| 39 |
+
BRIGHT_BLUE = "\033[94m"
|
| 40 |
+
BRIGHT_MAGENTA = "\033[95m"
|
| 41 |
+
BRIGHT_CYAN = "\033[96m"
|
| 42 |
+
BRIGHT_WHITE = "\033[97m"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class Agent:
|
| 46 |
+
"""Single agent with basic tools and MCP support."""
|
| 47 |
+
|
| 48 |
+
def __init__(
|
| 49 |
+
self,
|
| 50 |
+
llm_client: LLMClient,
|
| 51 |
+
system_prompt: str,
|
| 52 |
+
tools: list[Tool],
|
| 53 |
+
max_steps: int = 50,
|
| 54 |
+
workspace_dir: str = "./workspace",
|
| 55 |
+
token_limit: int = 80000, # Summary triggered when tokens exceed this value
|
| 56 |
+
):
|
| 57 |
+
self.llm = llm_client
|
| 58 |
+
self.tools = {tool.name: tool for tool in tools}
|
| 59 |
+
self.max_steps = max_steps
|
| 60 |
+
self.token_limit = token_limit
|
| 61 |
+
self.workspace_dir = Path(workspace_dir)
|
| 62 |
+
# Cancellation event for interrupting agent execution (set externally, e.g., by Esc key)
|
| 63 |
+
self.cancel_event: Optional[asyncio.Event] = None
|
| 64 |
+
|
| 65 |
+
# Ensure workspace exists
|
| 66 |
+
self.workspace_dir.mkdir(parents=True, exist_ok=True)
|
| 67 |
+
|
| 68 |
+
# Inject workspace information into system prompt if not already present
|
| 69 |
+
if "Current Workspace" not in system_prompt:
|
| 70 |
+
workspace_info = f"\n\n## Current Workspace\nYou are currently working in: `{self.workspace_dir.absolute()}`\nAll relative paths will be resolved relative to this directory."
|
| 71 |
+
system_prompt = system_prompt + workspace_info
|
| 72 |
+
|
| 73 |
+
self.system_prompt = system_prompt
|
| 74 |
+
|
| 75 |
+
# Initialize message history
|
| 76 |
+
self.messages: list[Message] = [Message(role="system", content=system_prompt)]
|
| 77 |
+
|
| 78 |
+
# Initialize logger
|
| 79 |
+
self.logger = AgentLogger()
|
| 80 |
+
|
| 81 |
+
# Token usage from last API response (updated after each LLM call)
|
| 82 |
+
self.api_total_tokens: int = 0
|
| 83 |
+
# Flag to skip token check right after summary (avoid consecutive triggers)
|
| 84 |
+
self._skip_next_token_check: bool = False
|
| 85 |
+
|
| 86 |
+
def add_user_message(self, content: str):
|
| 87 |
+
"""Add a user message to history."""
|
| 88 |
+
self.messages.append(Message(role="user", content=content))
|
| 89 |
+
|
| 90 |
+
def _check_cancelled(self) -> bool:
|
| 91 |
+
"""Check if agent execution has been cancelled.
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
True if cancelled, False otherwise.
|
| 95 |
+
"""
|
| 96 |
+
if self.cancel_event is not None and self.cancel_event.is_set():
|
| 97 |
+
return True
|
| 98 |
+
return False
|
| 99 |
+
|
| 100 |
+
def _cleanup_incomplete_messages(self):
|
| 101 |
+
"""Remove the incomplete assistant message and its partial tool results.
|
| 102 |
+
|
| 103 |
+
This ensures message consistency after cancellation by removing
|
| 104 |
+
only the current step's incomplete messages, preserving completed steps.
|
| 105 |
+
"""
|
| 106 |
+
# Find the index of the last assistant message
|
| 107 |
+
last_assistant_idx = -1
|
| 108 |
+
for i in range(len(self.messages) - 1, -1, -1):
|
| 109 |
+
if self.messages[i].role == "assistant":
|
| 110 |
+
last_assistant_idx = i
|
| 111 |
+
break
|
| 112 |
+
|
| 113 |
+
if last_assistant_idx == -1:
|
| 114 |
+
# No assistant message found, nothing to clean
|
| 115 |
+
return
|
| 116 |
+
|
| 117 |
+
# Remove the last assistant message and all tool results after it
|
| 118 |
+
removed_count = len(self.messages) - last_assistant_idx
|
| 119 |
+
if removed_count > 0:
|
| 120 |
+
self.messages = self.messages[:last_assistant_idx]
|
| 121 |
+
print(f"{Colors.DIM} Cleaned up {removed_count} incomplete message(s){Colors.RESET}")
|
| 122 |
+
|
| 123 |
+
def _estimate_tokens(self) -> int:
|
| 124 |
+
"""Accurately calculate token count for message history using tiktoken
|
| 125 |
+
|
| 126 |
+
Uses cl100k_base encoder (GPT-4/Claude/M2 compatible)
|
| 127 |
+
"""
|
| 128 |
+
try:
|
| 129 |
+
# Use cl100k_base encoder (used by GPT-4 and most modern models)
|
| 130 |
+
encoding = tiktoken.get_encoding("cl100k_base")
|
| 131 |
+
except Exception:
|
| 132 |
+
# Fallback: if tiktoken initialization fails, use simple estimation
|
| 133 |
+
return self._estimate_tokens_fallback()
|
| 134 |
+
|
| 135 |
+
total_tokens = 0
|
| 136 |
+
|
| 137 |
+
for msg in self.messages:
|
| 138 |
+
# Count text content
|
| 139 |
+
if isinstance(msg.content, str):
|
| 140 |
+
total_tokens += len(encoding.encode(msg.content))
|
| 141 |
+
elif isinstance(msg.content, list):
|
| 142 |
+
for block in msg.content:
|
| 143 |
+
if isinstance(block, dict):
|
| 144 |
+
# Convert dict to string for calculation
|
| 145 |
+
total_tokens += len(encoding.encode(str(block)))
|
| 146 |
+
|
| 147 |
+
# Count thinking
|
| 148 |
+
if msg.thinking:
|
| 149 |
+
total_tokens += len(encoding.encode(msg.thinking))
|
| 150 |
+
|
| 151 |
+
# Count tool_calls
|
| 152 |
+
if msg.tool_calls:
|
| 153 |
+
total_tokens += len(encoding.encode(str(msg.tool_calls)))
|
| 154 |
+
|
| 155 |
+
# Metadata overhead per message (approximately 4 tokens)
|
| 156 |
+
total_tokens += 4
|
| 157 |
+
|
| 158 |
+
return total_tokens
|
| 159 |
+
|
| 160 |
+
def _estimate_tokens_fallback(self) -> int:
|
| 161 |
+
"""Fallback token estimation method (when tiktoken is unavailable)"""
|
| 162 |
+
total_chars = 0
|
| 163 |
+
for msg in self.messages:
|
| 164 |
+
if isinstance(msg.content, str):
|
| 165 |
+
total_chars += len(msg.content)
|
| 166 |
+
elif isinstance(msg.content, list):
|
| 167 |
+
for block in msg.content:
|
| 168 |
+
if isinstance(block, dict):
|
| 169 |
+
total_chars += len(str(block))
|
| 170 |
+
|
| 171 |
+
if msg.thinking:
|
| 172 |
+
total_chars += len(msg.thinking)
|
| 173 |
+
|
| 174 |
+
if msg.tool_calls:
|
| 175 |
+
total_chars += len(str(msg.tool_calls))
|
| 176 |
+
|
| 177 |
+
# Rough estimation: average 2.5 characters = 1 token
|
| 178 |
+
return int(total_chars / 2.5)
|
| 179 |
+
|
| 180 |
+
async def _summarize_messages(self):
|
| 181 |
+
"""Message history summarization: summarize conversations between user messages when tokens exceed limit
|
| 182 |
+
|
| 183 |
+
Strategy (Agent mode):
|
| 184 |
+
- Keep all user messages (these are user intents)
|
| 185 |
+
- Summarize content between each user-user pair (agent execution process)
|
| 186 |
+
- If last round is still executing (has agent/tool messages but no next user), also summarize
|
| 187 |
+
- Structure: system -> user1 -> summary1 -> user2 -> summary2 -> user3 -> summary3 (if executing)
|
| 188 |
+
|
| 189 |
+
Summary is triggered when EITHER:
|
| 190 |
+
- Local token estimation exceeds limit
|
| 191 |
+
- API reported total_tokens exceeds limit
|
| 192 |
+
"""
|
| 193 |
+
# Skip check if we just completed a summary (wait for next LLM call to update api_total_tokens)
|
| 194 |
+
if self._skip_next_token_check:
|
| 195 |
+
self._skip_next_token_check = False
|
| 196 |
+
return
|
| 197 |
+
|
| 198 |
+
estimated_tokens = self._estimate_tokens()
|
| 199 |
+
|
| 200 |
+
# Check both local estimation and API reported tokens
|
| 201 |
+
should_summarize = estimated_tokens > self.token_limit or self.api_total_tokens > self.token_limit
|
| 202 |
+
|
| 203 |
+
# If neither exceeded, no summary needed
|
| 204 |
+
if not should_summarize:
|
| 205 |
+
return
|
| 206 |
+
|
| 207 |
+
print(
|
| 208 |
+
f"\n{Colors.BRIGHT_YELLOW}📊 Token usage - Local estimate: {estimated_tokens}, API reported: {self.api_total_tokens}, Limit: {self.token_limit}{Colors.RESET}"
|
| 209 |
+
)
|
| 210 |
+
print(f"{Colors.BRIGHT_YELLOW}🔄 Triggering message history summarization...{Colors.RESET}")
|
| 211 |
+
|
| 212 |
+
# Find all user message indices (skip system prompt)
|
| 213 |
+
user_indices = [i for i, msg in enumerate(self.messages) if msg.role == "user" and i > 0]
|
| 214 |
+
|
| 215 |
+
# Need at least 1 user message to perform summary
|
| 216 |
+
if len(user_indices) < 1:
|
| 217 |
+
print(f"{Colors.BRIGHT_YELLOW}⚠️ Insufficient messages, cannot summarize{Colors.RESET}")
|
| 218 |
+
return
|
| 219 |
+
|
| 220 |
+
# Build new message list
|
| 221 |
+
new_messages = [self.messages[0]] # Keep system prompt
|
| 222 |
+
summary_count = 0
|
| 223 |
+
|
| 224 |
+
# Iterate through each user message and summarize the execution process after it
|
| 225 |
+
for i, user_idx in enumerate(user_indices):
|
| 226 |
+
# Add current user message
|
| 227 |
+
new_messages.append(self.messages[user_idx])
|
| 228 |
+
|
| 229 |
+
# Determine message range to summarize
|
| 230 |
+
# If last user, go to end of message list; otherwise to before next user
|
| 231 |
+
if i < len(user_indices) - 1:
|
| 232 |
+
next_user_idx = user_indices[i + 1]
|
| 233 |
+
else:
|
| 234 |
+
next_user_idx = len(self.messages)
|
| 235 |
+
|
| 236 |
+
# Extract execution messages for this round
|
| 237 |
+
execution_messages = self.messages[user_idx + 1 : next_user_idx]
|
| 238 |
+
|
| 239 |
+
# If there are execution messages in this round, summarize them
|
| 240 |
+
if execution_messages:
|
| 241 |
+
summary_text = await self._create_summary(execution_messages, i + 1)
|
| 242 |
+
if summary_text:
|
| 243 |
+
summary_message = Message(
|
| 244 |
+
role="user",
|
| 245 |
+
content=f"[Assistant Execution Summary]\n\n{summary_text}",
|
| 246 |
+
)
|
| 247 |
+
new_messages.append(summary_message)
|
| 248 |
+
summary_count += 1
|
| 249 |
+
|
| 250 |
+
# Replace message list
|
| 251 |
+
self.messages = new_messages
|
| 252 |
+
|
| 253 |
+
# Skip next token check to avoid consecutive summary triggers
|
| 254 |
+
# (api_total_tokens will be updated after next LLM call)
|
| 255 |
+
self._skip_next_token_check = True
|
| 256 |
+
|
| 257 |
+
new_tokens = self._estimate_tokens()
|
| 258 |
+
print(f"{Colors.BRIGHT_GREEN}✓ Summary completed, local tokens: {estimated_tokens} → {new_tokens}{Colors.RESET}")
|
| 259 |
+
print(f"{Colors.DIM} Structure: system + {len(user_indices)} user messages + {summary_count} summaries{Colors.RESET}")
|
| 260 |
+
print(f"{Colors.DIM} Note: API token count will update on next LLM call{Colors.RESET}")
|
| 261 |
+
|
| 262 |
+
async def _create_summary(self, messages: list[Message], round_num: int) -> str:
|
| 263 |
+
"""Create summary for one execution round
|
| 264 |
+
|
| 265 |
+
Args:
|
| 266 |
+
messages: List of messages to summarize
|
| 267 |
+
round_num: Round number
|
| 268 |
+
|
| 269 |
+
Returns:
|
| 270 |
+
Summary text
|
| 271 |
+
"""
|
| 272 |
+
if not messages:
|
| 273 |
+
return ""
|
| 274 |
+
|
| 275 |
+
# Build summary content
|
| 276 |
+
summary_content = f"Round {round_num} execution process:\n\n"
|
| 277 |
+
for msg in messages:
|
| 278 |
+
if msg.role == "assistant":
|
| 279 |
+
content_text = msg.content if isinstance(msg.content, str) else str(msg.content)
|
| 280 |
+
summary_content += f"Assistant: {content_text}\n"
|
| 281 |
+
if msg.tool_calls:
|
| 282 |
+
tool_names = [tc.function.name for tc in msg.tool_calls]
|
| 283 |
+
summary_content += f" → Called tools: {', '.join(tool_names)}\n"
|
| 284 |
+
elif msg.role == "tool":
|
| 285 |
+
result_preview = msg.content if isinstance(msg.content, str) else str(msg.content)
|
| 286 |
+
summary_content += f" ← Tool returned: {result_preview}...\n"
|
| 287 |
+
|
| 288 |
+
# Call LLM to generate concise summary
|
| 289 |
+
try:
|
| 290 |
+
summary_prompt = f"""Please provide a concise summary of the following Agent execution process:
|
| 291 |
+
|
| 292 |
+
{summary_content}
|
| 293 |
+
|
| 294 |
+
Requirements:
|
| 295 |
+
1. Focus on what tasks were completed and which tools were called
|
| 296 |
+
2. Keep key execution results and important findings
|
| 297 |
+
3. Be concise and clear, within 1000 words
|
| 298 |
+
4. Use English
|
| 299 |
+
5. Do not include "user" related content, only summarize the Agent's execution process"""
|
| 300 |
+
|
| 301 |
+
summary_msg = Message(role="user", content=summary_prompt)
|
| 302 |
+
response = await self.llm.generate(
|
| 303 |
+
messages=[
|
| 304 |
+
Message(
|
| 305 |
+
role="system",
|
| 306 |
+
content="You are an assistant skilled at summarizing Agent execution processes.",
|
| 307 |
+
),
|
| 308 |
+
summary_msg,
|
| 309 |
+
]
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
summary_text = response.content
|
| 313 |
+
print(f"{Colors.BRIGHT_GREEN}✓ Summary for round {round_num} generated successfully{Colors.RESET}")
|
| 314 |
+
return summary_text
|
| 315 |
+
|
| 316 |
+
except Exception as e:
|
| 317 |
+
print(f"{Colors.BRIGHT_RED}✗ Summary generation failed for round {round_num}: {e}{Colors.RESET}")
|
| 318 |
+
# Use simple text summary on failure
|
| 319 |
+
return summary_content
|
| 320 |
+
|
| 321 |
+
async def run(self, cancel_event: Optional[asyncio.Event] = None) -> str:
|
| 322 |
+
"""Execute agent loop until task is complete or max steps reached.
|
| 323 |
+
|
| 324 |
+
Args:
|
| 325 |
+
cancel_event: Optional asyncio.Event that can be set to cancel execution.
|
| 326 |
+
When set, the agent will stop at the next safe checkpoint
|
| 327 |
+
(after completing the current step to keep messages consistent).
|
| 328 |
+
|
| 329 |
+
Returns:
|
| 330 |
+
The final response content, or error message (including cancellation message).
|
| 331 |
+
"""
|
| 332 |
+
# Set cancellation event (can also be set via self.cancel_event before calling run())
|
| 333 |
+
if cancel_event is not None:
|
| 334 |
+
self.cancel_event = cancel_event
|
| 335 |
+
|
| 336 |
+
# Start new run, initialize log file
|
| 337 |
+
self.logger.start_new_run()
|
| 338 |
+
print(f"{Colors.DIM}📝 Log file: {self.logger.get_log_file_path()}{Colors.RESET}")
|
| 339 |
+
|
| 340 |
+
step = 0
|
| 341 |
+
run_start_time = perf_counter()
|
| 342 |
+
|
| 343 |
+
while step < self.max_steps:
|
| 344 |
+
# Check for cancellation at start of each step
|
| 345 |
+
if self._check_cancelled():
|
| 346 |
+
self._cleanup_incomplete_messages()
|
| 347 |
+
cancel_msg = "Task cancelled by user."
|
| 348 |
+
print(f"\n{Colors.BRIGHT_YELLOW}⚠️ {cancel_msg}{Colors.RESET}")
|
| 349 |
+
return cancel_msg
|
| 350 |
+
|
| 351 |
+
step_start_time = perf_counter()
|
| 352 |
+
# Check and summarize message history to prevent context overflow
|
| 353 |
+
await self._summarize_messages()
|
| 354 |
+
|
| 355 |
+
# Step header with proper width calculation
|
| 356 |
+
BOX_WIDTH = 58
|
| 357 |
+
step_text = f"{Colors.BOLD}{Colors.BRIGHT_CYAN}💭 Step {step + 1}/{self.max_steps}{Colors.RESET}"
|
| 358 |
+
step_display_width = calculate_display_width(step_text)
|
| 359 |
+
padding = max(0, BOX_WIDTH - 1 - step_display_width) # -1 for leading space
|
| 360 |
+
|
| 361 |
+
print(f"\n{Colors.DIM}╭{'─' * BOX_WIDTH}╮{Colors.RESET}")
|
| 362 |
+
print(f"{Colors.DIM}│{Colors.RESET} {step_text}{' ' * padding}{Colors.DIM}│{Colors.RESET}")
|
| 363 |
+
print(f"{Colors.DIM}╰{'─' * BOX_WIDTH}╯{Colors.RESET}")
|
| 364 |
+
|
| 365 |
+
# Get tool list for LLM call
|
| 366 |
+
tool_list = list(self.tools.values())
|
| 367 |
+
|
| 368 |
+
# Log LLM request and call LLM with Tool objects directly
|
| 369 |
+
self.logger.log_request(messages=self.messages, tools=tool_list)
|
| 370 |
+
|
| 371 |
+
try:
|
| 372 |
+
response = await self.llm.generate(messages=self.messages, tools=tool_list)
|
| 373 |
+
except Exception as e:
|
| 374 |
+
# Check if it's a retry exhausted error
|
| 375 |
+
from .retry import RetryExhaustedError
|
| 376 |
+
|
| 377 |
+
if isinstance(e, RetryExhaustedError):
|
| 378 |
+
error_msg = f"LLM call failed after {e.attempts} retries\nLast error: {str(e.last_exception)}"
|
| 379 |
+
print(f"\n{Colors.BRIGHT_RED}❌ Retry failed:{Colors.RESET} {error_msg}")
|
| 380 |
+
else:
|
| 381 |
+
error_msg = f"LLM call failed: {str(e)}"
|
| 382 |
+
print(f"\n{Colors.BRIGHT_RED}❌ Error:{Colors.RESET} {error_msg}")
|
| 383 |
+
return error_msg
|
| 384 |
+
|
| 385 |
+
# Accumulate API reported token usage
|
| 386 |
+
if response.usage:
|
| 387 |
+
self.api_total_tokens = response.usage.total_tokens
|
| 388 |
+
|
| 389 |
+
# Log LLM response
|
| 390 |
+
self.logger.log_response(
|
| 391 |
+
content=response.content,
|
| 392 |
+
thinking=response.thinking,
|
| 393 |
+
tool_calls=response.tool_calls,
|
| 394 |
+
finish_reason=response.finish_reason,
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
# Add assistant message
|
| 398 |
+
assistant_msg = Message(
|
| 399 |
+
role="assistant",
|
| 400 |
+
content=response.content,
|
| 401 |
+
thinking=response.thinking,
|
| 402 |
+
tool_calls=response.tool_calls,
|
| 403 |
+
)
|
| 404 |
+
self.messages.append(assistant_msg)
|
| 405 |
+
|
| 406 |
+
# Print thinking if present
|
| 407 |
+
if response.thinking:
|
| 408 |
+
print(f"\n{Colors.BOLD}{Colors.MAGENTA}🧠 Thinking:{Colors.RESET}")
|
| 409 |
+
print(f"{Colors.DIM}{response.thinking}{Colors.RESET}")
|
| 410 |
+
|
| 411 |
+
# Print assistant response
|
| 412 |
+
if response.content:
|
| 413 |
+
print(f"\n{Colors.BOLD}{Colors.BRIGHT_BLUE}🤖 Assistant:{Colors.RESET}")
|
| 414 |
+
print(f"{response.content}")
|
| 415 |
+
|
| 416 |
+
# Check if task is complete (no tool calls)
|
| 417 |
+
if not response.tool_calls:
|
| 418 |
+
step_elapsed = perf_counter() - step_start_time
|
| 419 |
+
total_elapsed = perf_counter() - run_start_time
|
| 420 |
+
print(f"\n{Colors.DIM}⏱️ Step {step + 1} completed in {step_elapsed:.2f}s (total: {total_elapsed:.2f}s){Colors.RESET}")
|
| 421 |
+
return response.content
|
| 422 |
+
|
| 423 |
+
# Check for cancellation before executing tools
|
| 424 |
+
if self._check_cancelled():
|
| 425 |
+
self._cleanup_incomplete_messages()
|
| 426 |
+
cancel_msg = "Task cancelled by user."
|
| 427 |
+
print(f"\n{Colors.BRIGHT_YELLOW}⚠️ {cancel_msg}{Colors.RESET}")
|
| 428 |
+
return cancel_msg
|
| 429 |
+
|
| 430 |
+
# Execute tool calls
|
| 431 |
+
for tool_call in response.tool_calls:
|
| 432 |
+
tool_call_id = tool_call.id
|
| 433 |
+
function_name = tool_call.function.name
|
| 434 |
+
arguments = tool_call.function.arguments
|
| 435 |
+
|
| 436 |
+
# Tool call header
|
| 437 |
+
print(f"\n{Colors.BRIGHT_YELLOW}🔧 Tool Call:{Colors.RESET} {Colors.BOLD}{Colors.CYAN}{function_name}{Colors.RESET}")
|
| 438 |
+
|
| 439 |
+
# Arguments (formatted display)
|
| 440 |
+
print(f"{Colors.DIM} Arguments:{Colors.RESET}")
|
| 441 |
+
# Truncate each argument value to avoid overly long output
|
| 442 |
+
truncated_args = {}
|
| 443 |
+
for key, value in arguments.items():
|
| 444 |
+
value_str = str(value)
|
| 445 |
+
if len(value_str) > 200:
|
| 446 |
+
truncated_args[key] = value_str[:200] + "..."
|
| 447 |
+
else:
|
| 448 |
+
truncated_args[key] = value
|
| 449 |
+
args_json = json.dumps(truncated_args, indent=2, ensure_ascii=False)
|
| 450 |
+
for line in args_json.split("\n"):
|
| 451 |
+
print(f" {Colors.DIM}{line}{Colors.RESET}")
|
| 452 |
+
|
| 453 |
+
# Execute tool
|
| 454 |
+
if function_name not in self.tools:
|
| 455 |
+
result = ToolResult(
|
| 456 |
+
success=False,
|
| 457 |
+
content="",
|
| 458 |
+
error=f"Unknown tool: {function_name}",
|
| 459 |
+
)
|
| 460 |
+
else:
|
| 461 |
+
try:
|
| 462 |
+
tool = self.tools[function_name]
|
| 463 |
+
result = await tool.execute(**arguments)
|
| 464 |
+
except Exception as e:
|
| 465 |
+
# Catch all exceptions during tool execution, convert to failed ToolResult
|
| 466 |
+
import traceback
|
| 467 |
+
|
| 468 |
+
error_detail = f"{type(e).__name__}: {str(e)}"
|
| 469 |
+
error_trace = traceback.format_exc()
|
| 470 |
+
result = ToolResult(
|
| 471 |
+
success=False,
|
| 472 |
+
content="",
|
| 473 |
+
error=f"Tool execution failed: {error_detail}\n\nTraceback:\n{error_trace}",
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
# Log tool execution result
|
| 477 |
+
self.logger.log_tool_result(
|
| 478 |
+
tool_name=function_name,
|
| 479 |
+
arguments=arguments,
|
| 480 |
+
result_success=result.success,
|
| 481 |
+
result_content=result.content if result.success else None,
|
| 482 |
+
result_error=result.error if not result.success else None,
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
# Print result
|
| 486 |
+
if result.success:
|
| 487 |
+
result_text = result.content
|
| 488 |
+
if len(result_text) > 300:
|
| 489 |
+
result_text = result_text[:300] + f"{Colors.DIM}...{Colors.RESET}"
|
| 490 |
+
print(f"{Colors.BRIGHT_GREEN}✓ Result:{Colors.RESET} {result_text}")
|
| 491 |
+
else:
|
| 492 |
+
print(f"{Colors.BRIGHT_RED}✗ Error:{Colors.RESET} {Colors.RED}{result.error}{Colors.RESET}")
|
| 493 |
+
|
| 494 |
+
# Add tool result message
|
| 495 |
+
tool_msg = Message(
|
| 496 |
+
role="tool",
|
| 497 |
+
content=result.content if result.success else f"Error: {result.error}",
|
| 498 |
+
tool_call_id=tool_call_id,
|
| 499 |
+
name=function_name,
|
| 500 |
+
)
|
| 501 |
+
self.messages.append(tool_msg)
|
| 502 |
+
|
| 503 |
+
# Check for cancellation after each tool execution
|
| 504 |
+
if self._check_cancelled():
|
| 505 |
+
self._cleanup_incomplete_messages()
|
| 506 |
+
cancel_msg = "Task cancelled by user."
|
| 507 |
+
print(f"\n{Colors.BRIGHT_YELLOW}⚠️ {cancel_msg}{Colors.RESET}")
|
| 508 |
+
return cancel_msg
|
| 509 |
+
|
| 510 |
+
step_elapsed = perf_counter() - step_start_time
|
| 511 |
+
total_elapsed = perf_counter() - run_start_time
|
| 512 |
+
print(f"\n{Colors.DIM}⏱️ Step {step + 1} completed in {step_elapsed:.2f}s (total: {total_elapsed:.2f}s){Colors.RESET}")
|
| 513 |
+
|
| 514 |
+
step += 1
|
| 515 |
+
|
| 516 |
+
# Max steps reached
|
| 517 |
+
error_msg = f"Task couldn't be completed after {self.max_steps} steps."
|
| 518 |
+
print(f"\n{Colors.BRIGHT_YELLOW}⚠️ {error_msg}{Colors.RESET}")
|
| 519 |
+
return error_msg
|
| 520 |
+
|
| 521 |
+
def get_history(self) -> list[Message]:
|
| 522 |
+
"""Get message history."""
|
| 523 |
+
return self.messages.copy()
|
mini_agent/cli.py
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Mini Agent - Interactive Runtime Example
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
mini-agent [--workspace DIR]
|
| 6 |
+
|
| 7 |
+
Examples:
|
| 8 |
+
mini-agent # Use current directory as workspace
|
| 9 |
+
mini-agent --workspace /path/to/dir # Use specific workspace directory
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import asyncio
|
| 14 |
+
import platform
|
| 15 |
+
import subprocess
|
| 16 |
+
import sys
|
| 17 |
+
import threading
|
| 18 |
+
from datetime import datetime
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import List
|
| 21 |
+
|
| 22 |
+
from prompt_toolkit import PromptSession
|
| 23 |
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
| 24 |
+
from prompt_toolkit.completion import WordCompleter
|
| 25 |
+
from prompt_toolkit.history import FileHistory
|
| 26 |
+
from prompt_toolkit.key_binding import KeyBindings
|
| 27 |
+
from prompt_toolkit.styles import Style
|
| 28 |
+
|
| 29 |
+
from mini_agent import LLMClient
|
| 30 |
+
from mini_agent.agent import Agent
|
| 31 |
+
from mini_agent.config import Config
|
| 32 |
+
from mini_agent.schema import LLMProvider
|
| 33 |
+
from mini_agent.tools.base import Tool
|
| 34 |
+
from mini_agent.tools.bash_tool import BashKillTool, BashOutputTool, BashTool
|
| 35 |
+
from mini_agent.tools.file_tools import EditTool, ReadTool, WriteTool
|
| 36 |
+
from mini_agent.tools.mcp_loader import cleanup_mcp_connections, load_mcp_tools_async, set_mcp_timeout_config
|
| 37 |
+
from mini_agent.tools.note_tool import SessionNoteTool
|
| 38 |
+
from mini_agent.tools.skill_tool import create_skill_tools
|
| 39 |
+
from mini_agent.utils import calculate_display_width
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ANSI color codes
|
| 43 |
+
class Colors:
|
| 44 |
+
"""Terminal color definitions"""
|
| 45 |
+
|
| 46 |
+
RESET = "\033[0m"
|
| 47 |
+
BOLD = "\033[1m"
|
| 48 |
+
DIM = "\033[2m"
|
| 49 |
+
|
| 50 |
+
# Foreground colors
|
| 51 |
+
BLACK = "\033[30m"
|
| 52 |
+
RED = "\033[31m"
|
| 53 |
+
GREEN = "\033[32m"
|
| 54 |
+
YELLOW = "\033[33m"
|
| 55 |
+
BLUE = "\033[34m"
|
| 56 |
+
MAGENTA = "\033[35m"
|
| 57 |
+
CYAN = "\033[36m"
|
| 58 |
+
WHITE = "\033[37m"
|
| 59 |
+
|
| 60 |
+
# Bright colors
|
| 61 |
+
BRIGHT_BLACK = "\033[90m"
|
| 62 |
+
BRIGHT_RED = "\033[91m"
|
| 63 |
+
BRIGHT_GREEN = "\033[92m"
|
| 64 |
+
BRIGHT_YELLOW = "\033[93m"
|
| 65 |
+
BRIGHT_BLUE = "\033[94m"
|
| 66 |
+
BRIGHT_MAGENTA = "\033[95m"
|
| 67 |
+
BRIGHT_CYAN = "\033[96m"
|
| 68 |
+
BRIGHT_WHITE = "\033[97m"
|
| 69 |
+
|
| 70 |
+
# Background colors
|
| 71 |
+
BG_RED = "\033[41m"
|
| 72 |
+
BG_GREEN = "\033[42m"
|
| 73 |
+
BG_YELLOW = "\033[43m"
|
| 74 |
+
BG_BLUE = "\033[44m"
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def get_log_directory() -> Path:
|
| 78 |
+
"""Get the log directory path."""
|
| 79 |
+
return Path.home() / ".mini-agent" / "log"
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def show_log_directory(open_file_manager: bool = True) -> None:
|
| 83 |
+
"""Show log directory contents and optionally open file manager.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
open_file_manager: Whether to open the system file manager
|
| 87 |
+
"""
|
| 88 |
+
log_dir = get_log_directory()
|
| 89 |
+
|
| 90 |
+
print(f"\n{Colors.BRIGHT_CYAN}📁 Log Directory: {log_dir}{Colors.RESET}")
|
| 91 |
+
|
| 92 |
+
if not log_dir.exists() or not log_dir.is_dir():
|
| 93 |
+
print(f"{Colors.RED}Log directory does not exist: {log_dir}{Colors.RESET}\n")
|
| 94 |
+
return
|
| 95 |
+
|
| 96 |
+
log_files = list(log_dir.glob("*.log"))
|
| 97 |
+
|
| 98 |
+
if not log_files:
|
| 99 |
+
print(f"{Colors.YELLOW}No log files found in directory.{Colors.RESET}\n")
|
| 100 |
+
return
|
| 101 |
+
|
| 102 |
+
# Sort by modification time (newest first)
|
| 103 |
+
log_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
| 104 |
+
|
| 105 |
+
print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}")
|
| 106 |
+
print(f"{Colors.BOLD}{Colors.BRIGHT_YELLOW}Available Log Files (newest first):{Colors.RESET}")
|
| 107 |
+
|
| 108 |
+
for i, log_file in enumerate(log_files[:10], 1):
|
| 109 |
+
mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
|
| 110 |
+
size = log_file.stat().st_size
|
| 111 |
+
size_str = f"{size:,}" if size < 1024 else f"{size / 1024:.1f}K"
|
| 112 |
+
print(f" {Colors.GREEN}{i:2d}.{Colors.RESET} {Colors.BRIGHT_WHITE}{log_file.name}{Colors.RESET}")
|
| 113 |
+
print(f" {Colors.DIM}Modified: {mtime.strftime('%Y-%m-%d %H:%M:%S')}, Size: {size_str}{Colors.RESET}")
|
| 114 |
+
|
| 115 |
+
if len(log_files) > 10:
|
| 116 |
+
print(f" {Colors.DIM}... and {len(log_files) - 10} more files{Colors.RESET}")
|
| 117 |
+
|
| 118 |
+
print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}")
|
| 119 |
+
|
| 120 |
+
# Open file manager
|
| 121 |
+
if open_file_manager:
|
| 122 |
+
_open_directory_in_file_manager(log_dir)
|
| 123 |
+
|
| 124 |
+
print()
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _open_directory_in_file_manager(directory: Path) -> None:
|
| 128 |
+
"""Open directory in system file manager (cross-platform)."""
|
| 129 |
+
system = platform.system()
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
if system == "Darwin":
|
| 133 |
+
subprocess.run(["open", str(directory)], check=False)
|
| 134 |
+
elif system == "Windows":
|
| 135 |
+
subprocess.run(["explorer", str(directory)], check=False)
|
| 136 |
+
elif system == "Linux":
|
| 137 |
+
subprocess.run(["xdg-open", str(directory)], check=False)
|
| 138 |
+
except FileNotFoundError:
|
| 139 |
+
print(f"{Colors.YELLOW}Could not open file manager. Please navigate manually.{Colors.RESET}")
|
| 140 |
+
except Exception as e:
|
| 141 |
+
print(f"{Colors.YELLOW}Error opening file manager: {e}{Colors.RESET}")
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def read_log_file(filename: str) -> None:
|
| 145 |
+
"""Read and display a specific log file.
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
filename: The log filename to read
|
| 149 |
+
"""
|
| 150 |
+
log_dir = get_log_directory()
|
| 151 |
+
log_file = log_dir / filename
|
| 152 |
+
|
| 153 |
+
if not log_file.exists() or not log_file.is_file():
|
| 154 |
+
print(f"\n{Colors.RED}❌ Log file not found: {log_file}{Colors.RESET}\n")
|
| 155 |
+
return
|
| 156 |
+
|
| 157 |
+
print(f"\n{Colors.BRIGHT_CYAN}📄 Reading: {log_file}{Colors.RESET}")
|
| 158 |
+
print(f"{Colors.DIM}{'─' * 80}{Colors.RESET}")
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
with open(log_file, "r", encoding="utf-8") as f:
|
| 162 |
+
content = f.read()
|
| 163 |
+
print(content)
|
| 164 |
+
print(f"{Colors.DIM}{'─' * 80}{Colors.RESET}")
|
| 165 |
+
print(f"\n{Colors.GREEN}✅ End of file{Colors.RESET}\n")
|
| 166 |
+
except Exception as e:
|
| 167 |
+
print(f"\n{Colors.RED}❌ Error reading file: {e}{Colors.RESET}\n")
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def print_banner():
|
| 171 |
+
"""Print welcome banner with proper alignment"""
|
| 172 |
+
BOX_WIDTH = 58
|
| 173 |
+
banner_text = f"{Colors.BOLD}🤖 Mini Agent - Multi-turn Interactive Session{Colors.RESET}"
|
| 174 |
+
banner_width = calculate_display_width(banner_text)
|
| 175 |
+
|
| 176 |
+
# Center the text with proper padding
|
| 177 |
+
total_padding = BOX_WIDTH - banner_width
|
| 178 |
+
left_padding = total_padding // 2
|
| 179 |
+
right_padding = total_padding - left_padding
|
| 180 |
+
|
| 181 |
+
print()
|
| 182 |
+
print(f"{Colors.BOLD}{Colors.BRIGHT_CYAN}╔{'═' * BOX_WIDTH}╗{Colors.RESET}")
|
| 183 |
+
print(
|
| 184 |
+
f"{Colors.BOLD}{Colors.BRIGHT_CYAN}║{Colors.RESET}{' ' * left_padding}{banner_text}{' ' * right_padding}{Colors.BOLD}{Colors.BRIGHT_CYAN}║{Colors.RESET}"
|
| 185 |
+
)
|
| 186 |
+
print(f"{Colors.BOLD}{Colors.BRIGHT_CYAN}╚{'═' * BOX_WIDTH}╝{Colors.RESET}")
|
| 187 |
+
print()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def print_help():
|
| 191 |
+
"""Print help information"""
|
| 192 |
+
help_text = f"""
|
| 193 |
+
{Colors.BOLD}{Colors.BRIGHT_YELLOW}Available Commands:{Colors.RESET}
|
| 194 |
+
{Colors.BRIGHT_GREEN}/help{Colors.RESET} - Show this help message
|
| 195 |
+
{Colors.BRIGHT_GREEN}/clear{Colors.RESET} - Clear session history (keep system prompt)
|
| 196 |
+
{Colors.BRIGHT_GREEN}/history{Colors.RESET} - Show current session message count
|
| 197 |
+
{Colors.BRIGHT_GREEN}/stats{Colors.RESET} - Show session statistics
|
| 198 |
+
{Colors.BRIGHT_GREEN}/log{Colors.RESET} - Show log directory and recent files
|
| 199 |
+
{Colors.BRIGHT_GREEN}/log <file>{Colors.RESET} - Read a specific log file
|
| 200 |
+
{Colors.BRIGHT_GREEN}/exit{Colors.RESET} - Exit program (also: exit, quit, q)
|
| 201 |
+
|
| 202 |
+
{Colors.BOLD}{Colors.BRIGHT_YELLOW}Keyboard Shortcuts:{Colors.RESET}
|
| 203 |
+
{Colors.BRIGHT_CYAN}Esc{Colors.RESET} - Cancel current agent execution
|
| 204 |
+
{Colors.BRIGHT_CYAN}Ctrl+C{Colors.RESET} - Exit program
|
| 205 |
+
{Colors.BRIGHT_CYAN}Ctrl+U{Colors.RESET} - Clear current input line
|
| 206 |
+
{Colors.BRIGHT_CYAN}Ctrl+L{Colors.RESET} - Clear screen
|
| 207 |
+
{Colors.BRIGHT_CYAN}Ctrl+J{Colors.RESET} - Insert newline (also Ctrl+Enter)
|
| 208 |
+
{Colors.BRIGHT_CYAN}Tab{Colors.RESET} - Auto-complete commands
|
| 209 |
+
{Colors.BRIGHT_CYAN}↑/↓{Colors.RESET} - Browse command history
|
| 210 |
+
{Colors.BRIGHT_CYAN}→{Colors.RESET} - Accept auto-suggestion
|
| 211 |
+
|
| 212 |
+
{Colors.BOLD}{Colors.BRIGHT_YELLOW}Usage:{Colors.RESET}
|
| 213 |
+
- Enter your task directly, Agent will help you complete it
|
| 214 |
+
- Agent remembers all conversation content in this session
|
| 215 |
+
- Use {Colors.BRIGHT_GREEN}/clear{Colors.RESET} to start a new session
|
| 216 |
+
- Press {Colors.BRIGHT_CYAN}Enter{Colors.RESET} to submit your message
|
| 217 |
+
- Use {Colors.BRIGHT_CYAN}Ctrl+J{Colors.RESET} to insert line breaks within your message
|
| 218 |
+
"""
|
| 219 |
+
print(help_text)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def print_session_info(agent: Agent, workspace_dir: Path, model: str):
|
| 223 |
+
"""Print session information with proper alignment"""
|
| 224 |
+
BOX_WIDTH = 58
|
| 225 |
+
|
| 226 |
+
def print_info_line(text: str):
|
| 227 |
+
"""Print a single info line with proper padding"""
|
| 228 |
+
# Account for leading space
|
| 229 |
+
text_width = calculate_display_width(text)
|
| 230 |
+
padding = max(0, BOX_WIDTH - 1 - text_width)
|
| 231 |
+
print(f"{Colors.DIM}│{Colors.RESET} {text}{' ' * padding}{Colors.DIM}│{Colors.RESET}")
|
| 232 |
+
|
| 233 |
+
# Top border
|
| 234 |
+
print(f"{Colors.DIM}┌{'─' * BOX_WIDTH}┐{Colors.RESET}")
|
| 235 |
+
|
| 236 |
+
# Header (centered)
|
| 237 |
+
header_text = f"{Colors.BRIGHT_CYAN}Session Info{Colors.RESET}"
|
| 238 |
+
header_width = calculate_display_width(header_text)
|
| 239 |
+
header_padding_total = BOX_WIDTH - 1 - header_width # -1 for leading space
|
| 240 |
+
header_padding_left = header_padding_total // 2
|
| 241 |
+
header_padding_right = header_padding_total - header_padding_left
|
| 242 |
+
print(f"{Colors.DIM}│{Colors.RESET} {' ' * header_padding_left}{header_text}{' ' * header_padding_right}{Colors.DIM}│{Colors.RESET}")
|
| 243 |
+
|
| 244 |
+
# Divider
|
| 245 |
+
print(f"{Colors.DIM}├{'─' * BOX_WIDTH}┤{Colors.RESET}")
|
| 246 |
+
|
| 247 |
+
# Info lines
|
| 248 |
+
print_info_line(f"Model: {model}")
|
| 249 |
+
print_info_line(f"Workspace: {workspace_dir}")
|
| 250 |
+
print_info_line(f"Message History: {len(agent.messages)} messages")
|
| 251 |
+
print_info_line(f"Available Tools: {len(agent.tools)} tools")
|
| 252 |
+
|
| 253 |
+
# Bottom border
|
| 254 |
+
print(f"{Colors.DIM}└{'─' * BOX_WIDTH}┘{Colors.RESET}")
|
| 255 |
+
print()
|
| 256 |
+
print(f"{Colors.DIM}Type {Colors.BRIGHT_GREEN}/help{Colors.DIM} for help, {Colors.BRIGHT_GREEN}/exit{Colors.DIM} to quit{Colors.RESET}")
|
| 257 |
+
print()
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def print_stats(agent: Agent, session_start: datetime):
|
| 261 |
+
"""Print session statistics"""
|
| 262 |
+
duration = datetime.now() - session_start
|
| 263 |
+
hours, remainder = divmod(int(duration.total_seconds()), 3600)
|
| 264 |
+
minutes, seconds = divmod(remainder, 60)
|
| 265 |
+
|
| 266 |
+
# Count different types of messages
|
| 267 |
+
user_msgs = sum(1 for m in agent.messages if m.role == "user")
|
| 268 |
+
assistant_msgs = sum(1 for m in agent.messages if m.role == "assistant")
|
| 269 |
+
tool_msgs = sum(1 for m in agent.messages if m.role == "tool")
|
| 270 |
+
|
| 271 |
+
print(f"\n{Colors.BOLD}{Colors.BRIGHT_CYAN}Session Statistics:{Colors.RESET}")
|
| 272 |
+
print(f"{Colors.DIM}{'─' * 40}{Colors.RESET}")
|
| 273 |
+
print(f" Session Duration: {hours:02d}:{minutes:02d}:{seconds:02d}")
|
| 274 |
+
print(f" Total Messages: {len(agent.messages)}")
|
| 275 |
+
print(f" - User Messages: {Colors.BRIGHT_GREEN}{user_msgs}{Colors.RESET}")
|
| 276 |
+
print(f" - Assistant Replies: {Colors.BRIGHT_BLUE}{assistant_msgs}{Colors.RESET}")
|
| 277 |
+
print(f" - Tool Calls: {Colors.BRIGHT_YELLOW}{tool_msgs}{Colors.RESET}")
|
| 278 |
+
print(f" Available Tools: {len(agent.tools)}")
|
| 279 |
+
if agent.api_total_tokens > 0:
|
| 280 |
+
print(f" API Tokens Used: {Colors.BRIGHT_MAGENTA}{agent.api_total_tokens:,}{Colors.RESET}")
|
| 281 |
+
print(f"{Colors.DIM}{'─' * 40}{Colors.RESET}\n")
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def parse_args() -> argparse.Namespace:
|
| 285 |
+
"""Parse command line arguments
|
| 286 |
+
|
| 287 |
+
Returns:
|
| 288 |
+
Parsed arguments
|
| 289 |
+
"""
|
| 290 |
+
parser = argparse.ArgumentParser(
|
| 291 |
+
description="Mini Agent - AI assistant with file tools and MCP support",
|
| 292 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 293 |
+
epilog="""
|
| 294 |
+
Examples:
|
| 295 |
+
mini-agent # Use current directory as workspace
|
| 296 |
+
mini-agent --workspace /path/to/dir # Use specific workspace directory
|
| 297 |
+
mini-agent log # Show log directory and recent files
|
| 298 |
+
mini-agent log agent_run_xxx.log # Read a specific log file
|
| 299 |
+
""",
|
| 300 |
+
)
|
| 301 |
+
parser.add_argument(
|
| 302 |
+
"--workspace",
|
| 303 |
+
"-w",
|
| 304 |
+
type=str,
|
| 305 |
+
default=None,
|
| 306 |
+
help="Workspace directory (default: current directory)",
|
| 307 |
+
)
|
| 308 |
+
parser.add_argument(
|
| 309 |
+
"--version",
|
| 310 |
+
"-v",
|
| 311 |
+
action="version",
|
| 312 |
+
version="mini-agent 0.1.0",
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
# Subcommands
|
| 316 |
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
| 317 |
+
|
| 318 |
+
# log subcommand
|
| 319 |
+
log_parser = subparsers.add_parser("log", help="Show log directory or read log files")
|
| 320 |
+
log_parser.add_argument(
|
| 321 |
+
"filename",
|
| 322 |
+
nargs="?",
|
| 323 |
+
default=None,
|
| 324 |
+
help="Log filename to read (optional, shows directory if omitted)",
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
return parser.parse_args()
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
async def initialize_base_tools(config: Config):
|
| 331 |
+
"""Initialize base tools (independent of workspace)
|
| 332 |
+
|
| 333 |
+
These tools are loaded from package configuration and don't depend on workspace.
|
| 334 |
+
Note: File tools are now workspace-dependent and initialized in add_workspace_tools()
|
| 335 |
+
|
| 336 |
+
Args:
|
| 337 |
+
config: Configuration object
|
| 338 |
+
|
| 339 |
+
Returns:
|
| 340 |
+
Tuple of (list of tools, skill loader if skills enabled)
|
| 341 |
+
"""
|
| 342 |
+
|
| 343 |
+
tools = []
|
| 344 |
+
skill_loader = None
|
| 345 |
+
|
| 346 |
+
# 1. Bash tool and Bash Output tool
|
| 347 |
+
if config.tools.enable_bash:
|
| 348 |
+
bash_tool = BashTool()
|
| 349 |
+
tools.append(bash_tool)
|
| 350 |
+
print(f"{Colors.GREEN}✅ Loaded Bash tool{Colors.RESET}")
|
| 351 |
+
|
| 352 |
+
bash_output_tool = BashOutputTool()
|
| 353 |
+
tools.append(bash_output_tool)
|
| 354 |
+
print(f"{Colors.GREEN}✅ Loaded Bash Output tool{Colors.RESET}")
|
| 355 |
+
|
| 356 |
+
bash_kill_tool = BashKillTool()
|
| 357 |
+
tools.append(bash_kill_tool)
|
| 358 |
+
print(f"{Colors.GREEN}✅ Loaded Bash Kill tool{Colors.RESET}")
|
| 359 |
+
|
| 360 |
+
# 3. Claude Skills (loaded from package directory)
|
| 361 |
+
if config.tools.enable_skills:
|
| 362 |
+
print(f"{Colors.BRIGHT_CYAN}Loading Claude Skills...{Colors.RESET}")
|
| 363 |
+
try:
|
| 364 |
+
# Resolve skills directory with priority search
|
| 365 |
+
# Expand ~ to user home directory for portability
|
| 366 |
+
skills_path = Path(config.tools.skills_dir).expanduser()
|
| 367 |
+
if skills_path.is_absolute():
|
| 368 |
+
skills_dir = str(skills_path)
|
| 369 |
+
else:
|
| 370 |
+
# Search in priority order:
|
| 371 |
+
# 1. Current directory (dev mode: ./skills or ./mini_agent/skills)
|
| 372 |
+
# 2. Package directory (installed: site-packages/mini_agent/skills)
|
| 373 |
+
search_paths = [
|
| 374 |
+
skills_path, # ./skills for backward compatibility
|
| 375 |
+
Path("mini_agent") / skills_path, # ./mini_agent/skills
|
| 376 |
+
Config.get_package_dir() / skills_path, # site-packages/mini_agent/skills
|
| 377 |
+
]
|
| 378 |
+
|
| 379 |
+
# Find first existing path
|
| 380 |
+
skills_dir = str(skills_path) # default
|
| 381 |
+
for path in search_paths:
|
| 382 |
+
if path.exists():
|
| 383 |
+
skills_dir = str(path.resolve())
|
| 384 |
+
break
|
| 385 |
+
|
| 386 |
+
skill_tools, skill_loader = create_skill_tools(skills_dir)
|
| 387 |
+
if skill_tools:
|
| 388 |
+
tools.extend(skill_tools)
|
| 389 |
+
print(f"{Colors.GREEN}✅ Loaded Skill tool (get_skill){Colors.RESET}")
|
| 390 |
+
else:
|
| 391 |
+
print(f"{Colors.YELLOW}⚠️ No available Skills found{Colors.RESET}")
|
| 392 |
+
except Exception as e:
|
| 393 |
+
print(f"{Colors.YELLOW}⚠️ Failed to load Skills: {e}{Colors.RESET}")
|
| 394 |
+
|
| 395 |
+
# 4. MCP tools (loaded with priority search)
|
| 396 |
+
if config.tools.enable_mcp:
|
| 397 |
+
print(f"{Colors.BRIGHT_CYAN}Loading MCP tools...{Colors.RESET}")
|
| 398 |
+
try:
|
| 399 |
+
# Apply MCP timeout configuration from config.yaml
|
| 400 |
+
mcp_config = config.tools.mcp
|
| 401 |
+
set_mcp_timeout_config(
|
| 402 |
+
connect_timeout=mcp_config.connect_timeout,
|
| 403 |
+
execute_timeout=mcp_config.execute_timeout,
|
| 404 |
+
sse_read_timeout=mcp_config.sse_read_timeout,
|
| 405 |
+
)
|
| 406 |
+
print(
|
| 407 |
+
f"{Colors.DIM} MCP timeouts: connect={mcp_config.connect_timeout}s, "
|
| 408 |
+
f"execute={mcp_config.execute_timeout}s, sse_read={mcp_config.sse_read_timeout}s{Colors.RESET}"
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
# Use priority search for mcp.json
|
| 412 |
+
mcp_config_path = Config.find_config_file(config.tools.mcp_config_path)
|
| 413 |
+
if mcp_config_path:
|
| 414 |
+
mcp_tools = await load_mcp_tools_async(str(mcp_config_path))
|
| 415 |
+
if mcp_tools:
|
| 416 |
+
tools.extend(mcp_tools)
|
| 417 |
+
print(f"{Colors.GREEN}✅ Loaded {len(mcp_tools)} MCP tools (from: {mcp_config_path}){Colors.RESET}")
|
| 418 |
+
else:
|
| 419 |
+
print(f"{Colors.YELLOW}⚠️ No available MCP tools found{Colors.RESET}")
|
| 420 |
+
else:
|
| 421 |
+
print(f"{Colors.YELLOW}⚠️ MCP config file not found: {config.tools.mcp_config_path}{Colors.RESET}")
|
| 422 |
+
except Exception as e:
|
| 423 |
+
print(f"{Colors.YELLOW}⚠️ Failed to load MCP tools: {e}{Colors.RESET}")
|
| 424 |
+
|
| 425 |
+
print() # Empty line separator
|
| 426 |
+
return tools, skill_loader
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def add_workspace_tools(tools: List[Tool], config: Config, workspace_dir: Path):
|
| 430 |
+
"""Add workspace-dependent tools
|
| 431 |
+
|
| 432 |
+
These tools need to know the workspace directory.
|
| 433 |
+
|
| 434 |
+
Args:
|
| 435 |
+
tools: Existing tools list to add to
|
| 436 |
+
config: Configuration object
|
| 437 |
+
workspace_dir: Workspace directory path
|
| 438 |
+
"""
|
| 439 |
+
# Ensure workspace directory exists
|
| 440 |
+
workspace_dir.mkdir(parents=True, exist_ok=True)
|
| 441 |
+
|
| 442 |
+
# File tools - need workspace to resolve relative paths
|
| 443 |
+
if config.tools.enable_file_tools:
|
| 444 |
+
tools.extend(
|
| 445 |
+
[
|
| 446 |
+
ReadTool(workspace_dir=str(workspace_dir)),
|
| 447 |
+
WriteTool(workspace_dir=str(workspace_dir)),
|
| 448 |
+
EditTool(workspace_dir=str(workspace_dir)),
|
| 449 |
+
]
|
| 450 |
+
)
|
| 451 |
+
print(f"{Colors.GREEN}✅ Loaded file operation tools (workspace: {workspace_dir}){Colors.RESET}")
|
| 452 |
+
|
| 453 |
+
# Session note tool - needs workspace to store memory file
|
| 454 |
+
if config.tools.enable_note:
|
| 455 |
+
tools.append(SessionNoteTool(memory_file=str(workspace_dir / ".agent_memory.json")))
|
| 456 |
+
print(f"{Colors.GREEN}✅ Loaded session note tool{Colors.RESET}")
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
async def run_agent(workspace_dir: Path):
|
| 460 |
+
"""Run interactive Agent
|
| 461 |
+
|
| 462 |
+
Args:
|
| 463 |
+
workspace_dir: Workspace directory path
|
| 464 |
+
"""
|
| 465 |
+
session_start = datetime.now()
|
| 466 |
+
|
| 467 |
+
# 1. Load configuration from package directory
|
| 468 |
+
config_path = Config.get_default_config_path()
|
| 469 |
+
|
| 470 |
+
if not config_path.exists():
|
| 471 |
+
print(f"{Colors.RED}❌ Configuration file not found{Colors.RESET}")
|
| 472 |
+
print()
|
| 473 |
+
print(f"{Colors.BRIGHT_CYAN}📦 Configuration Search Path:{Colors.RESET}")
|
| 474 |
+
print(f" {Colors.DIM}1) mini_agent/config/config.yaml{Colors.RESET} (development)")
|
| 475 |
+
print(f" {Colors.DIM}2) ~/.mini-agent/config/config.yaml{Colors.RESET} (user)")
|
| 476 |
+
print(f" {Colors.DIM}3) <package>/config/config.yaml{Colors.RESET} (installed)")
|
| 477 |
+
print()
|
| 478 |
+
print(f"{Colors.BRIGHT_YELLOW}🚀 Quick Setup (Recommended):{Colors.RESET}")
|
| 479 |
+
print(
|
| 480 |
+
f" {Colors.BRIGHT_GREEN}curl -fsSL https://raw.githubusercontent.com/MiniMax-AI/Mini-Agent/main/scripts/setup-config.sh | bash{Colors.RESET}"
|
| 481 |
+
)
|
| 482 |
+
print()
|
| 483 |
+
print(f"{Colors.DIM} This will automatically:{Colors.RESET}")
|
| 484 |
+
print(f"{Colors.DIM} • Create ~/.mini-agent/config/{Colors.RESET}")
|
| 485 |
+
print(f"{Colors.DIM} • Download configuration files{Colors.RESET}")
|
| 486 |
+
print(f"{Colors.DIM} • Guide you to add your API Key{Colors.RESET}")
|
| 487 |
+
print()
|
| 488 |
+
print(f"{Colors.BRIGHT_YELLOW}📝 Manual Setup:{Colors.RESET}")
|
| 489 |
+
user_config_dir = Path.home() / ".mini-agent" / "config"
|
| 490 |
+
example_config = Config.get_package_dir() / "config" / "config-example.yaml"
|
| 491 |
+
print(f" {Colors.DIM}mkdir -p {user_config_dir}{Colors.RESET}")
|
| 492 |
+
print(f" {Colors.DIM}cp {example_config} {user_config_dir}/config.yaml{Colors.RESET}")
|
| 493 |
+
print(f" {Colors.DIM}# Then edit {user_config_dir}/config.yaml to add your API Key{Colors.RESET}")
|
| 494 |
+
print()
|
| 495 |
+
return
|
| 496 |
+
|
| 497 |
+
try:
|
| 498 |
+
config = Config.from_yaml(config_path)
|
| 499 |
+
except FileNotFoundError:
|
| 500 |
+
print(f"{Colors.RED}❌ Error: Configuration file not found: {config_path}{Colors.RESET}")
|
| 501 |
+
return
|
| 502 |
+
except ValueError as e:
|
| 503 |
+
print(f"{Colors.RED}❌ Error: {e}{Colors.RESET}")
|
| 504 |
+
print(f"{Colors.YELLOW}Please check the configuration file format{Colors.RESET}")
|
| 505 |
+
return
|
| 506 |
+
except Exception as e:
|
| 507 |
+
print(f"{Colors.RED}❌ Error: Failed to load configuration file: {e}{Colors.RESET}")
|
| 508 |
+
return
|
| 509 |
+
|
| 510 |
+
# 2. Initialize LLM client
|
| 511 |
+
from mini_agent.retry import RetryConfig as RetryConfigBase
|
| 512 |
+
|
| 513 |
+
# Convert configuration format
|
| 514 |
+
retry_config = RetryConfigBase(
|
| 515 |
+
enabled=config.llm.retry.enabled,
|
| 516 |
+
max_retries=config.llm.retry.max_retries,
|
| 517 |
+
initial_delay=config.llm.retry.initial_delay,
|
| 518 |
+
max_delay=config.llm.retry.max_delay,
|
| 519 |
+
exponential_base=config.llm.retry.exponential_base,
|
| 520 |
+
retryable_exceptions=(Exception,),
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
# Create retry callback function to display retry information in terminal
|
| 524 |
+
def on_retry(exception: Exception, attempt: int):
|
| 525 |
+
"""Retry callback function to display retry information"""
|
| 526 |
+
print(f"\n{Colors.BRIGHT_YELLOW}⚠️ LLM call failed (attempt {attempt}): {str(exception)}{Colors.RESET}")
|
| 527 |
+
next_delay = retry_config.calculate_delay(attempt - 1)
|
| 528 |
+
print(f"{Colors.DIM} Retrying in {next_delay:.1f}s (attempt {attempt + 1})...{Colors.RESET}")
|
| 529 |
+
|
| 530 |
+
# Convert provider string to LLMProvider enum
|
| 531 |
+
provider = LLMProvider.ANTHROPIC if config.llm.provider.lower() == "anthropic" else LLMProvider.OPENAI
|
| 532 |
+
|
| 533 |
+
llm_client = LLMClient(
|
| 534 |
+
api_key=config.llm.api_key,
|
| 535 |
+
provider=provider,
|
| 536 |
+
api_base=config.llm.api_base,
|
| 537 |
+
model=config.llm.model,
|
| 538 |
+
retry_config=retry_config if config.llm.retry.enabled else None,
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
# Set retry callback
|
| 542 |
+
if config.llm.retry.enabled:
|
| 543 |
+
llm_client.retry_callback = on_retry
|
| 544 |
+
print(f"{Colors.GREEN}✅ LLM retry mechanism enabled (max {config.llm.retry.max_retries} retries){Colors.RESET}")
|
| 545 |
+
|
| 546 |
+
# 3. Initialize base tools (independent of workspace)
|
| 547 |
+
tools, skill_loader = await initialize_base_tools(config)
|
| 548 |
+
|
| 549 |
+
# 4. Add workspace-dependent tools
|
| 550 |
+
add_workspace_tools(tools, config, workspace_dir)
|
| 551 |
+
|
| 552 |
+
# 5. Load System Prompt (with priority search)
|
| 553 |
+
system_prompt_path = Config.find_config_file(config.agent.system_prompt_path)
|
| 554 |
+
if system_prompt_path and system_prompt_path.exists():
|
| 555 |
+
system_prompt = system_prompt_path.read_text(encoding="utf-8")
|
| 556 |
+
print(f"{Colors.GREEN}✅ Loaded system prompt (from: {system_prompt_path}){Colors.RESET}")
|
| 557 |
+
else:
|
| 558 |
+
system_prompt = "You are Mini-Agent, an intelligent assistant powered by MiniMax M2.1 that can help users complete various tasks."
|
| 559 |
+
print(f"{Colors.YELLOW}⚠️ System prompt not found, using default{Colors.RESET}")
|
| 560 |
+
|
| 561 |
+
# 6. Inject Skills Metadata into System Prompt (Progressive Disclosure - Level 1)
|
| 562 |
+
if skill_loader:
|
| 563 |
+
skills_metadata = skill_loader.get_skills_metadata_prompt()
|
| 564 |
+
if skills_metadata:
|
| 565 |
+
# Replace placeholder with actual metadata
|
| 566 |
+
system_prompt = system_prompt.replace("{SKILLS_METADATA}", skills_metadata)
|
| 567 |
+
print(f"{Colors.GREEN}✅ Injected {len(skill_loader.loaded_skills)} skills metadata into system prompt{Colors.RESET}")
|
| 568 |
+
else:
|
| 569 |
+
# Remove placeholder if no skills
|
| 570 |
+
system_prompt = system_prompt.replace("{SKILLS_METADATA}", "")
|
| 571 |
+
else:
|
| 572 |
+
# Remove placeholder if skills not enabled
|
| 573 |
+
system_prompt = system_prompt.replace("{SKILLS_METADATA}", "")
|
| 574 |
+
|
| 575 |
+
# 7. Create Agent
|
| 576 |
+
agent = Agent(
|
| 577 |
+
llm_client=llm_client,
|
| 578 |
+
system_prompt=system_prompt,
|
| 579 |
+
tools=tools,
|
| 580 |
+
max_steps=config.agent.max_steps,
|
| 581 |
+
workspace_dir=str(workspace_dir),
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
# 8. Display welcome information
|
| 585 |
+
print_banner()
|
| 586 |
+
print_session_info(agent, workspace_dir, config.llm.model)
|
| 587 |
+
|
| 588 |
+
# 9. Setup prompt_toolkit session
|
| 589 |
+
# Command completer
|
| 590 |
+
command_completer = WordCompleter(
|
| 591 |
+
["/help", "/clear", "/history", "/stats", "/log", "/exit", "/quit", "/q"],
|
| 592 |
+
ignore_case=True,
|
| 593 |
+
sentence=True,
|
| 594 |
+
)
|
| 595 |
+
|
| 596 |
+
# Custom style for prompt
|
| 597 |
+
prompt_style = Style.from_dict(
|
| 598 |
+
{
|
| 599 |
+
"prompt": "#00ff00 bold", # Green and bold
|
| 600 |
+
"separator": "#666666", # Gray
|
| 601 |
+
}
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
# Custom key bindings
|
| 605 |
+
kb = KeyBindings()
|
| 606 |
+
|
| 607 |
+
@kb.add("c-u") # Ctrl+U: Clear current line
|
| 608 |
+
def _(event):
|
| 609 |
+
"""Clear the current input line"""
|
| 610 |
+
event.current_buffer.reset()
|
| 611 |
+
|
| 612 |
+
@kb.add("c-l") # Ctrl+L: Clear screen (optional bonus)
|
| 613 |
+
def _(event):
|
| 614 |
+
"""Clear the screen"""
|
| 615 |
+
event.app.renderer.clear()
|
| 616 |
+
|
| 617 |
+
@kb.add("c-j") # Ctrl+J (对应 Ctrl+Enter)
|
| 618 |
+
def _(event):
|
| 619 |
+
"""Insert a newline"""
|
| 620 |
+
event.current_buffer.insert_text("\n")
|
| 621 |
+
|
| 622 |
+
# Create prompt session with history and auto-suggest
|
| 623 |
+
# Use FileHistory for persistent history across sessions (stored in user's home directory)
|
| 624 |
+
history_file = Path.home() / ".mini-agent" / ".history"
|
| 625 |
+
history_file.parent.mkdir(parents=True, exist_ok=True)
|
| 626 |
+
session = PromptSession(
|
| 627 |
+
history=FileHistory(str(history_file)),
|
| 628 |
+
auto_suggest=AutoSuggestFromHistory(),
|
| 629 |
+
completer=command_completer,
|
| 630 |
+
style=prompt_style,
|
| 631 |
+
key_bindings=kb,
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
# 10. Interactive loop
|
| 635 |
+
while True:
|
| 636 |
+
try:
|
| 637 |
+
# Get user input using prompt_toolkit
|
| 638 |
+
user_input = await session.prompt_async(
|
| 639 |
+
[
|
| 640 |
+
("class:prompt", "You"),
|
| 641 |
+
("", " › "),
|
| 642 |
+
],
|
| 643 |
+
multiline=False,
|
| 644 |
+
enable_history_search=True,
|
| 645 |
+
)
|
| 646 |
+
user_input = user_input.strip()
|
| 647 |
+
|
| 648 |
+
if not user_input:
|
| 649 |
+
continue
|
| 650 |
+
|
| 651 |
+
# Handle commands
|
| 652 |
+
if user_input.startswith("/"):
|
| 653 |
+
command = user_input.lower()
|
| 654 |
+
|
| 655 |
+
if command in ["/exit", "/quit", "/q"]:
|
| 656 |
+
print(f"\n{Colors.BRIGHT_YELLOW}👋 Goodbye! Thanks for using Mini Agent{Colors.RESET}\n")
|
| 657 |
+
print_stats(agent, session_start)
|
| 658 |
+
break
|
| 659 |
+
|
| 660 |
+
elif command == "/help":
|
| 661 |
+
print_help()
|
| 662 |
+
continue
|
| 663 |
+
|
| 664 |
+
elif command == "/clear":
|
| 665 |
+
# Clear message history but keep system prompt
|
| 666 |
+
old_count = len(agent.messages)
|
| 667 |
+
agent.messages = [agent.messages[0]] # Keep only system message
|
| 668 |
+
print(f"{Colors.GREEN}✅ Cleared {old_count - 1} messages, starting new session{Colors.RESET}\n")
|
| 669 |
+
continue
|
| 670 |
+
|
| 671 |
+
elif command == "/history":
|
| 672 |
+
print(f"\n{Colors.BRIGHT_CYAN}Current session message count: {len(agent.messages)}{Colors.RESET}\n")
|
| 673 |
+
continue
|
| 674 |
+
|
| 675 |
+
elif command == "/stats":
|
| 676 |
+
print_stats(agent, session_start)
|
| 677 |
+
continue
|
| 678 |
+
|
| 679 |
+
elif command == "/log" or command.startswith("/log "):
|
| 680 |
+
# Parse /log command
|
| 681 |
+
parts = user_input.split(maxsplit=1)
|
| 682 |
+
if len(parts) == 1:
|
| 683 |
+
# /log - show log directory
|
| 684 |
+
show_log_directory(open_file_manager=True)
|
| 685 |
+
else:
|
| 686 |
+
# /log <filename> - read specific log file
|
| 687 |
+
filename = parts[1].strip("\"'")
|
| 688 |
+
read_log_file(filename)
|
| 689 |
+
continue
|
| 690 |
+
|
| 691 |
+
else:
|
| 692 |
+
print(f"{Colors.RED}❌ Unknown command: {user_input}{Colors.RESET}")
|
| 693 |
+
print(f"{Colors.DIM}Type /help to see available commands{Colors.RESET}\n")
|
| 694 |
+
continue
|
| 695 |
+
|
| 696 |
+
# Normal conversation - exit check
|
| 697 |
+
if user_input.lower() in ["exit", "quit", "q"]:
|
| 698 |
+
print(f"\n{Colors.BRIGHT_YELLOW}👋 Goodbye! Thanks for using Mini Agent{Colors.RESET}\n")
|
| 699 |
+
print_stats(agent, session_start)
|
| 700 |
+
break
|
| 701 |
+
|
| 702 |
+
# Run Agent with Esc cancellation support
|
| 703 |
+
print(
|
| 704 |
+
f"\n{Colors.BRIGHT_BLUE}Agent{Colors.RESET} {Colors.DIM}›{Colors.RESET} {Colors.DIM}Thinking... (Esc to cancel){Colors.RESET}\n"
|
| 705 |
+
)
|
| 706 |
+
agent.add_user_message(user_input)
|
| 707 |
+
|
| 708 |
+
# Create cancellation event
|
| 709 |
+
cancel_event = asyncio.Event()
|
| 710 |
+
agent.cancel_event = cancel_event
|
| 711 |
+
|
| 712 |
+
# Esc key listener thread
|
| 713 |
+
esc_listener_stop = threading.Event()
|
| 714 |
+
esc_cancelled = [False] # Mutable container for thread access
|
| 715 |
+
|
| 716 |
+
def esc_key_listener():
|
| 717 |
+
"""Listen for Esc key in a separate thread."""
|
| 718 |
+
if platform.system() == "Windows":
|
| 719 |
+
try:
|
| 720 |
+
import msvcrt
|
| 721 |
+
|
| 722 |
+
while not esc_listener_stop.is_set():
|
| 723 |
+
if msvcrt.kbhit():
|
| 724 |
+
char = msvcrt.getch()
|
| 725 |
+
if char == b"\x1b": # Esc
|
| 726 |
+
print(f"\n{Colors.BRIGHT_YELLOW}⏹️ Esc pressed, cancelling...{Colors.RESET}")
|
| 727 |
+
esc_cancelled[0] = True
|
| 728 |
+
cancel_event.set()
|
| 729 |
+
break
|
| 730 |
+
esc_listener_stop.wait(0.05)
|
| 731 |
+
except Exception:
|
| 732 |
+
pass
|
| 733 |
+
return
|
| 734 |
+
|
| 735 |
+
# Unix/macOS
|
| 736 |
+
try:
|
| 737 |
+
import select
|
| 738 |
+
import termios
|
| 739 |
+
import tty
|
| 740 |
+
|
| 741 |
+
fd = sys.stdin.fileno()
|
| 742 |
+
old_settings = termios.tcgetattr(fd)
|
| 743 |
+
|
| 744 |
+
try:
|
| 745 |
+
tty.setcbreak(fd)
|
| 746 |
+
while not esc_listener_stop.is_set():
|
| 747 |
+
rlist, _, _ = select.select([sys.stdin], [], [], 0.05)
|
| 748 |
+
if rlist:
|
| 749 |
+
char = sys.stdin.read(1)
|
| 750 |
+
if char == "\x1b": # Esc
|
| 751 |
+
print(f"\n{Colors.BRIGHT_YELLOW}⏹️ Esc pressed, cancelling...{Colors.RESET}")
|
| 752 |
+
esc_cancelled[0] = True
|
| 753 |
+
cancel_event.set()
|
| 754 |
+
break
|
| 755 |
+
finally:
|
| 756 |
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
| 757 |
+
except Exception:
|
| 758 |
+
pass
|
| 759 |
+
|
| 760 |
+
# Start Esc listener thread
|
| 761 |
+
esc_thread = threading.Thread(target=esc_key_listener, daemon=True)
|
| 762 |
+
esc_thread.start()
|
| 763 |
+
|
| 764 |
+
# Run agent with periodic cancellation check
|
| 765 |
+
try:
|
| 766 |
+
agent_task = asyncio.create_task(agent.run())
|
| 767 |
+
|
| 768 |
+
# Poll for cancellation while agent runs
|
| 769 |
+
while not agent_task.done():
|
| 770 |
+
if esc_cancelled[0]:
|
| 771 |
+
cancel_event.set()
|
| 772 |
+
await asyncio.sleep(0.1)
|
| 773 |
+
|
| 774 |
+
# Get result
|
| 775 |
+
_ = agent_task.result()
|
| 776 |
+
|
| 777 |
+
except asyncio.CancelledError:
|
| 778 |
+
print(f"\n{Colors.BRIGHT_YELLOW}⚠️ Agent execution cancelled{Colors.RESET}")
|
| 779 |
+
finally:
|
| 780 |
+
agent.cancel_event = None
|
| 781 |
+
esc_listener_stop.set()
|
| 782 |
+
esc_thread.join(timeout=0.2)
|
| 783 |
+
|
| 784 |
+
# Visual separation
|
| 785 |
+
print(f"\n{Colors.DIM}{'─' * 60}{Colors.RESET}\n")
|
| 786 |
+
|
| 787 |
+
except KeyboardInterrupt:
|
| 788 |
+
print(f"\n\n{Colors.BRIGHT_YELLOW}👋 Interrupt signal detected, exiting...{Colors.RESET}\n")
|
| 789 |
+
print_stats(agent, session_start)
|
| 790 |
+
break
|
| 791 |
+
|
| 792 |
+
except Exception as e:
|
| 793 |
+
print(f"\n{Colors.RED}❌ Error: {e}{Colors.RESET}")
|
| 794 |
+
print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")
|
| 795 |
+
|
| 796 |
+
# 11. Cleanup MCP connections
|
| 797 |
+
try:
|
| 798 |
+
print(f"{Colors.BRIGHT_CYAN}Cleaning up MCP connections...{Colors.RESET}")
|
| 799 |
+
await cleanup_mcp_connections()
|
| 800 |
+
print(f"{Colors.GREEN}✅ Cleanup complete{Colors.RESET}\n")
|
| 801 |
+
except Exception as e:
|
| 802 |
+
print(f"{Colors.YELLOW}Error during cleanup (can be ignored): {e}{Colors.RESET}\n")
|
| 803 |
+
|
| 804 |
+
|
| 805 |
+
def main():
|
| 806 |
+
"""Main entry point for CLI"""
|
| 807 |
+
# Parse command line arguments
|
| 808 |
+
args = parse_args()
|
| 809 |
+
|
| 810 |
+
# Handle log subcommand
|
| 811 |
+
if args.command == "log":
|
| 812 |
+
if args.filename:
|
| 813 |
+
read_log_file(args.filename)
|
| 814 |
+
else:
|
| 815 |
+
show_log_directory(open_file_manager=True)
|
| 816 |
+
return
|
| 817 |
+
|
| 818 |
+
# Determine workspace directory
|
| 819 |
+
# Expand ~ to user home directory for portability
|
| 820 |
+
if args.workspace:
|
| 821 |
+
workspace_dir = Path(args.workspace).expanduser().absolute()
|
| 822 |
+
else:
|
| 823 |
+
# Use current working directory
|
| 824 |
+
workspace_dir = Path.cwd()
|
| 825 |
+
|
| 826 |
+
# Ensure workspace directory exists
|
| 827 |
+
workspace_dir.mkdir(parents=True, exist_ok=True)
|
| 828 |
+
|
| 829 |
+
# Run the agent (config always loaded from package directory)
|
| 830 |
+
asyncio.run(run_agent(workspace_dir))
|
| 831 |
+
|
| 832 |
+
|
| 833 |
+
if __name__ == "__main__":
|
| 834 |
+
main()
|
mini_agent/config.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration management module
|
| 2 |
+
|
| 3 |
+
Provides unified configuration loading and management functionality
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import yaml
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class RetryConfig(BaseModel):
|
| 13 |
+
"""Retry configuration"""
|
| 14 |
+
|
| 15 |
+
enabled: bool = True
|
| 16 |
+
max_retries: int = 3
|
| 17 |
+
initial_delay: float = 1.0
|
| 18 |
+
max_delay: float = 60.0
|
| 19 |
+
exponential_base: float = 2.0
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class LLMConfig(BaseModel):
|
| 23 |
+
"""LLM configuration"""
|
| 24 |
+
|
| 25 |
+
api_key: str
|
| 26 |
+
api_base: str = "https://api.minimax.io"
|
| 27 |
+
model: str = "MiniMax-M2.1"
|
| 28 |
+
provider: str = "anthropic" # "anthropic" or "openai"
|
| 29 |
+
retry: RetryConfig = Field(default_factory=RetryConfig)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class AgentConfig(BaseModel):
|
| 33 |
+
"""Agent configuration"""
|
| 34 |
+
|
| 35 |
+
max_steps: int = 50
|
| 36 |
+
workspace_dir: str = "./workspace"
|
| 37 |
+
system_prompt_path: str = "system_prompt.md"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class MCPConfig(BaseModel):
|
| 41 |
+
"""MCP (Model Context Protocol) timeout configuration"""
|
| 42 |
+
|
| 43 |
+
connect_timeout: float = 10.0 # Connection timeout (seconds)
|
| 44 |
+
execute_timeout: float = 60.0 # Tool execution timeout (seconds)
|
| 45 |
+
sse_read_timeout: float = 120.0 # SSE read timeout (seconds)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class ToolsConfig(BaseModel):
|
| 49 |
+
"""Tools configuration"""
|
| 50 |
+
|
| 51 |
+
# Basic tools (file operations, bash)
|
| 52 |
+
enable_file_tools: bool = True
|
| 53 |
+
enable_bash: bool = True
|
| 54 |
+
enable_note: bool = True
|
| 55 |
+
|
| 56 |
+
# Skills
|
| 57 |
+
enable_skills: bool = True
|
| 58 |
+
skills_dir: str = "./skills"
|
| 59 |
+
|
| 60 |
+
# MCP tools
|
| 61 |
+
enable_mcp: bool = True
|
| 62 |
+
mcp_config_path: str = "mcp.json"
|
| 63 |
+
mcp: MCPConfig = Field(default_factory=MCPConfig)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class Config(BaseModel):
|
| 67 |
+
"""Main configuration class"""
|
| 68 |
+
|
| 69 |
+
llm: LLMConfig
|
| 70 |
+
agent: AgentConfig
|
| 71 |
+
tools: ToolsConfig
|
| 72 |
+
|
| 73 |
+
@classmethod
|
| 74 |
+
def load(cls) -> "Config":
|
| 75 |
+
"""Load configuration from the default search path."""
|
| 76 |
+
config_path = cls.get_default_config_path()
|
| 77 |
+
if not config_path.exists():
|
| 78 |
+
raise FileNotFoundError("Configuration file not found. Run scripts/setup-config.sh or place config.yaml in mini_agent/config/.")
|
| 79 |
+
return cls.from_yaml(config_path)
|
| 80 |
+
|
| 81 |
+
@classmethod
|
| 82 |
+
def from_yaml(cls, config_path: str | Path) -> "Config":
|
| 83 |
+
"""Load configuration from YAML file
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
config_path: Configuration file path
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
Config instance
|
| 90 |
+
|
| 91 |
+
Raises:
|
| 92 |
+
FileNotFoundError: Configuration file does not exist
|
| 93 |
+
ValueError: Invalid configuration format or missing required fields
|
| 94 |
+
"""
|
| 95 |
+
config_path = Path(config_path)
|
| 96 |
+
|
| 97 |
+
if not config_path.exists():
|
| 98 |
+
raise FileNotFoundError(f"Configuration file does not exist: {config_path}")
|
| 99 |
+
|
| 100 |
+
with open(config_path, encoding="utf-8") as f:
|
| 101 |
+
data = yaml.safe_load(f)
|
| 102 |
+
|
| 103 |
+
if not data:
|
| 104 |
+
raise ValueError("Configuration file is empty")
|
| 105 |
+
|
| 106 |
+
# Parse LLM configuration
|
| 107 |
+
if "api_key" not in data:
|
| 108 |
+
raise ValueError("Configuration file missing required field: api_key")
|
| 109 |
+
|
| 110 |
+
if not data["api_key"] or data["api_key"] == "YOUR_API_KEY_HERE":
|
| 111 |
+
raise ValueError("Please configure a valid API Key")
|
| 112 |
+
|
| 113 |
+
# Parse retry configuration
|
| 114 |
+
retry_data = data.get("retry", {})
|
| 115 |
+
retry_config = RetryConfig(
|
| 116 |
+
enabled=retry_data.get("enabled", True),
|
| 117 |
+
max_retries=retry_data.get("max_retries", 3),
|
| 118 |
+
initial_delay=retry_data.get("initial_delay", 1.0),
|
| 119 |
+
max_delay=retry_data.get("max_delay", 60.0),
|
| 120 |
+
exponential_base=retry_data.get("exponential_base", 2.0),
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
llm_config = LLMConfig(
|
| 124 |
+
api_key=data["api_key"],
|
| 125 |
+
api_base=data.get("api_base", "https://api.minimax.io"),
|
| 126 |
+
model=data.get("model", "MiniMax-M2.1"),
|
| 127 |
+
provider=data.get("provider", "anthropic"),
|
| 128 |
+
retry=retry_config,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
# Parse Agent configuration
|
| 132 |
+
agent_config = AgentConfig(
|
| 133 |
+
max_steps=data.get("max_steps", 50),
|
| 134 |
+
workspace_dir=data.get("workspace_dir", "./workspace"),
|
| 135 |
+
system_prompt_path=data.get("system_prompt_path", "system_prompt.md"),
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# Parse tools configuration
|
| 139 |
+
tools_data = data.get("tools", {})
|
| 140 |
+
|
| 141 |
+
# Parse MCP configuration
|
| 142 |
+
mcp_data = tools_data.get("mcp", {})
|
| 143 |
+
mcp_config = MCPConfig(
|
| 144 |
+
connect_timeout=mcp_data.get("connect_timeout", 10.0),
|
| 145 |
+
execute_timeout=mcp_data.get("execute_timeout", 60.0),
|
| 146 |
+
sse_read_timeout=mcp_data.get("sse_read_timeout", 120.0),
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
tools_config = ToolsConfig(
|
| 150 |
+
enable_file_tools=tools_data.get("enable_file_tools", True),
|
| 151 |
+
enable_bash=tools_data.get("enable_bash", True),
|
| 152 |
+
enable_note=tools_data.get("enable_note", True),
|
| 153 |
+
enable_skills=tools_data.get("enable_skills", True),
|
| 154 |
+
skills_dir=tools_data.get("skills_dir", "./skills"),
|
| 155 |
+
enable_mcp=tools_data.get("enable_mcp", True),
|
| 156 |
+
mcp_config_path=tools_data.get("mcp_config_path", "mcp.json"),
|
| 157 |
+
mcp=mcp_config,
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
return cls(
|
| 161 |
+
llm=llm_config,
|
| 162 |
+
agent=agent_config,
|
| 163 |
+
tools=tools_config,
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
@staticmethod
|
| 167 |
+
def get_package_dir() -> Path:
|
| 168 |
+
"""Get the package installation directory
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
Path to the mini_agent package directory
|
| 172 |
+
"""
|
| 173 |
+
# Get the directory where this config.py file is located
|
| 174 |
+
return Path(__file__).parent
|
| 175 |
+
|
| 176 |
+
@classmethod
|
| 177 |
+
def find_config_file(cls, filename: str) -> Path | None:
|
| 178 |
+
"""Find configuration file with priority order
|
| 179 |
+
|
| 180 |
+
Search for config file in the following order of priority:
|
| 181 |
+
1) mini_agent/config/{filename} in current directory (development mode)
|
| 182 |
+
2) ~/.mini-agent/config/{filename} in user home directory
|
| 183 |
+
3) {package}/mini_agent/config/{filename} in package installation directory
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
filename: Configuration file name (e.g., "config.yaml", "mcp.json", "system_prompt.md")
|
| 187 |
+
|
| 188 |
+
Returns:
|
| 189 |
+
Path to found config file, or None if not found
|
| 190 |
+
"""
|
| 191 |
+
# Priority 1: Development mode - current directory's config/ subdirectory
|
| 192 |
+
dev_config = Path.cwd() / "mini_agent" / "config" / filename
|
| 193 |
+
if dev_config.exists():
|
| 194 |
+
return dev_config
|
| 195 |
+
|
| 196 |
+
# Priority 2: User config directory
|
| 197 |
+
user_config = Path.home() / ".mini-agent" / "config" / filename
|
| 198 |
+
if user_config.exists():
|
| 199 |
+
return user_config
|
| 200 |
+
|
| 201 |
+
# Priority 3: Package installation directory's config/ subdirectory
|
| 202 |
+
package_config = cls.get_package_dir() / "config" / filename
|
| 203 |
+
if package_config.exists():
|
| 204 |
+
return package_config
|
| 205 |
+
|
| 206 |
+
return None
|
| 207 |
+
|
| 208 |
+
@classmethod
|
| 209 |
+
def get_default_config_path(cls) -> Path:
|
| 210 |
+
"""Get the default config file path with priority search
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
Path to config.yaml (prioritizes: dev config/ > user config/ > package config/)
|
| 214 |
+
"""
|
| 215 |
+
config_path = cls.find_config_file("config.yaml")
|
| 216 |
+
if config_path:
|
| 217 |
+
return config_path
|
| 218 |
+
|
| 219 |
+
# Fallback to package config directory for error message purposes
|
| 220 |
+
return cls.get_package_dir() / "config" / "config.yaml"
|
mini_agent/config/config-example.yaml
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Mini Agent Configuration Example
|
| 2 |
+
#
|
| 3 |
+
# Configuration File Locations (in priority order):
|
| 4 |
+
# 1) mini_agent/config/config.yaml - Development mode (current directory)
|
| 5 |
+
# 2) ~/.mini-agent/config/config.yaml - User config directory
|
| 6 |
+
# 3) <package>/mini_agent/config/config.yaml - Package installation directory
|
| 7 |
+
#
|
| 8 |
+
# To use this config:
|
| 9 |
+
# - Copy this file to one of the above locations as config.yaml
|
| 10 |
+
# - Fill in your API key and customize settings as needed
|
| 11 |
+
# - All config files (config.yaml, mcp.json, system_prompt.md) are in the same directory
|
| 12 |
+
|
| 13 |
+
# ===== LLM Configuration =====
|
| 14 |
+
# MiniMax API Configuration
|
| 15 |
+
# MiniMax provides both global and China platforms:
|
| 16 |
+
# - Global: https://platform.minimax.io -> api_base: https://api.minimax.io
|
| 17 |
+
# - China: https://platform.minimaxi.com -> api_base: https://api.minimaxi.com
|
| 18 |
+
# Please choose based on your network environment and get API key from corresponding platform
|
| 19 |
+
api_key: "YOUR_API_KEY_HERE" # Replace with your MiniMax API Key
|
| 20 |
+
api_base: "https://api.minimax.io" # Global users (default)
|
| 21 |
+
# api_base: "https://api.minimaxi.com" # China users
|
| 22 |
+
model: "MiniMax-M2.1"
|
| 23 |
+
# LLM provider: "anthropic" or "openai"
|
| 24 |
+
# For MiniMax API, the suffix (/anthropic or /v1) is auto-appended based on provider.
|
| 25 |
+
# For third-party APIs (e.g., https://api.siliconflow.cn/v1), api_base is used as-is.
|
| 26 |
+
provider: "anthropic" # Default: anthropic
|
| 27 |
+
|
| 28 |
+
# ===== Retry Configuration =====
|
| 29 |
+
retry:
|
| 30 |
+
enabled: true # Enable retry mechanism
|
| 31 |
+
max_retries: 3 # Maximum number of retries
|
| 32 |
+
initial_delay: 1.0 # Initial delay time (seconds)
|
| 33 |
+
max_delay: 60.0 # Maximum delay time (seconds)
|
| 34 |
+
exponential_base: 2.0 # Exponential backoff base (delay = initial_delay * base^attempt)
|
| 35 |
+
|
| 36 |
+
# ===== Agent Configuration =====
|
| 37 |
+
max_steps: 100 # Maximum execution steps
|
| 38 |
+
workspace_dir: "./workspace" # Working directory
|
| 39 |
+
system_prompt_path: "system_prompt.md" # System prompt file (same config directory)
|
| 40 |
+
|
| 41 |
+
# ===== Tools Configuration =====
|
| 42 |
+
tools:
|
| 43 |
+
# Basic tool switches
|
| 44 |
+
enable_file_tools: true # File read/write/edit tools (ReadTool, WriteTool, EditTool)
|
| 45 |
+
enable_bash: true # Bash command execution tool
|
| 46 |
+
enable_note: true # Session note tool (SessionNoteTool)
|
| 47 |
+
|
| 48 |
+
# Claude Skills
|
| 49 |
+
enable_skills: true # Enable Skills
|
| 50 |
+
skills_dir: "./skills" # Skills directory path
|
| 51 |
+
|
| 52 |
+
# MCP Tools
|
| 53 |
+
enable_mcp: true # Enable MCP tools
|
| 54 |
+
mcp_config_path: "mcp.json" # MCP configuration file (same config directory)
|
| 55 |
+
# Note: API Keys for MCP tools are configured in mcp.json
|
| 56 |
+
# MCP timeout configuration (prevents hanging on network issues)
|
| 57 |
+
mcp:
|
| 58 |
+
connect_timeout: 10.0 # Connection timeout in seconds (default: 10)
|
| 59 |
+
execute_timeout: 60.0 # Tool execution timeout in seconds (default: 60)
|
| 60 |
+
sse_read_timeout: 120.0 # SSE read timeout in seconds (default: 120)
|
mini_agent/config/mcp-example.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mcpServers": {
|
| 3 |
+
"minimax_search": {
|
| 4 |
+
"description": "MiniMax Search - Powerful web search and intelligent browsing ⭐",
|
| 5 |
+
"type": "stdio",
|
| 6 |
+
"command": "uvx",
|
| 7 |
+
"args": [
|
| 8 |
+
"--from",
|
| 9 |
+
"git+https://github.com/MiniMax-AI/minimax_search",
|
| 10 |
+
"minimax-search"
|
| 11 |
+
],
|
| 12 |
+
"env": {
|
| 13 |
+
"JINA_API_KEY": "",
|
| 14 |
+
"SERPER_API_KEY": "",
|
| 15 |
+
"MINIMAX_API_KEY": ""
|
| 16 |
+
},
|
| 17 |
+
"disabled": true
|
| 18 |
+
},
|
| 19 |
+
"memory": {
|
| 20 |
+
"description": "Memory - Knowledge graph memory system (long-term memory based on graph database)",
|
| 21 |
+
"command": "npx",
|
| 22 |
+
"args": [
|
| 23 |
+
"-y",
|
| 24 |
+
"@modelcontextprotocol/server-memory"
|
| 25 |
+
],
|
| 26 |
+
"disabled": true
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
mini_agent/config/system_prompt.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are Mini-Agent, a versatile AI assistant powered by MiniMax, capable of executing complex tasks through a rich toolset and specialized skills.
|
| 2 |
+
|
| 3 |
+
## Core Capabilities
|
| 4 |
+
|
| 5 |
+
### 1. **Basic Tools**
|
| 6 |
+
- **File Operations**: Read, write, edit files with full path support
|
| 7 |
+
- **Bash Execution**: Run commands, manage git, packages, and system operations
|
| 8 |
+
- **MCP Tools**: Access additional tools from configured MCP servers
|
| 9 |
+
|
| 10 |
+
### 2. **Specialized Skills**
|
| 11 |
+
You have access to specialized skills that provide expert guidance and capabilities for specific tasks.
|
| 12 |
+
|
| 13 |
+
Skills are loaded dynamically using **Progressive Disclosure**:
|
| 14 |
+
- **Level 1 (Metadata)**: You see skill names and descriptions (below) at startup
|
| 15 |
+
- **Level 2 (Full Content)**: Load a skill's complete guidance using `get_skill(skill_name)`
|
| 16 |
+
- **Level 3+ (Resources)**: Skills may reference additional files and scripts as needed
|
| 17 |
+
|
| 18 |
+
**How to Use Skills:**
|
| 19 |
+
1. Check the metadata below to identify relevant skills for your task
|
| 20 |
+
2. Call `get_skill(skill_name)` to load the full guidance
|
| 21 |
+
3. Follow the skill's instructions and use appropriate tools (bash, file operations, etc.)
|
| 22 |
+
|
| 23 |
+
**Important Notes:**
|
| 24 |
+
- Skills provide expert patterns and procedural knowledge
|
| 25 |
+
- **For Python skills** (pdf, pptx, docx, xlsx, canvas-design, algorithmic-art): Setup Python environment FIRST (see Python Environment Management below)
|
| 26 |
+
- Skills may reference scripts and resources - use bash or read_file to access them
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
{SKILLS_METADATA}
|
| 31 |
+
|
| 32 |
+
## Working Guidelines
|
| 33 |
+
|
| 34 |
+
### Task Execution
|
| 35 |
+
1. **Analyze** the request and identify if a skill can help
|
| 36 |
+
2. **Break down** complex tasks into clear, executable steps
|
| 37 |
+
3. **Use skills** when appropriate for specialized guidance
|
| 38 |
+
4. **Execute** tools systematically and check results
|
| 39 |
+
5. **Report** progress and any issues encountered
|
| 40 |
+
|
| 41 |
+
### File Operations
|
| 42 |
+
- Use absolute paths or workspace-relative paths
|
| 43 |
+
- Verify file existence before reading/editing
|
| 44 |
+
- Create parent directories before writing files
|
| 45 |
+
- Handle errors gracefully with clear messages
|
| 46 |
+
|
| 47 |
+
### Bash Commands
|
| 48 |
+
- Explain destructive operations before execution
|
| 49 |
+
- Check command outputs for errors
|
| 50 |
+
- Use appropriate error handling
|
| 51 |
+
- Prefer specialized tools over raw commands when available
|
| 52 |
+
|
| 53 |
+
### Python Environment Management
|
| 54 |
+
**CRITICAL - Use `uv` for all Python operations. Before executing Python code:**
|
| 55 |
+
1. Check/create venv: `if [ ! -d .venv ]; then uv venv; fi`
|
| 56 |
+
2. Install packages: `uv pip install <package>`
|
| 57 |
+
3. Run scripts: `uv run python script.py`
|
| 58 |
+
4. If uv missing: `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
| 59 |
+
|
| 60 |
+
**Python-based skills:** pdf, pptx, docx, xlsx, canvas-design, algorithmic-art
|
| 61 |
+
|
| 62 |
+
### Communication
|
| 63 |
+
- Be concise but thorough in responses
|
| 64 |
+
- Explain your approach before tool execution
|
| 65 |
+
- Report errors with context and solutions
|
| 66 |
+
- Summarize accomplishments when complete
|
| 67 |
+
|
| 68 |
+
### Best Practices
|
| 69 |
+
- **Don't guess** - use tools to discover missing information
|
| 70 |
+
- **Be proactive** - infer intent and take reasonable actions
|
| 71 |
+
- **Stay focused** - stop when the task is fulfilled
|
| 72 |
+
- **Use skills** - leverage specialized knowledge when relevant
|
| 73 |
+
|
| 74 |
+
## Workspace Context
|
| 75 |
+
You are working in a workspace directory. All operations are relative to this context unless absolute paths are specified.
|
mini_agent/llm/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LLM clients package supporting both Anthropic and OpenAI protocols."""
|
| 2 |
+
|
| 3 |
+
from .anthropic_client import AnthropicClient
|
| 4 |
+
from .base import LLMClientBase
|
| 5 |
+
from .llm_wrapper import LLMClient
|
| 6 |
+
from .openai_client import OpenAIClient
|
| 7 |
+
|
| 8 |
+
__all__ = ["LLMClientBase", "AnthropicClient", "OpenAIClient", "LLMClient"]
|
| 9 |
+
|
mini_agent/llm/anthropic_client.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Anthropic LLM client implementation."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import anthropic
|
| 7 |
+
|
| 8 |
+
from ..retry import RetryConfig, async_retry
|
| 9 |
+
from ..schema import FunctionCall, LLMResponse, Message, TokenUsage, ToolCall
|
| 10 |
+
from .base import LLMClientBase
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class AnthropicClient(LLMClientBase):
|
| 16 |
+
"""LLM client using Anthropic's protocol.
|
| 17 |
+
|
| 18 |
+
This client uses the official Anthropic SDK and supports:
|
| 19 |
+
- Extended thinking content
|
| 20 |
+
- Tool calling
|
| 21 |
+
- Retry logic
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
api_key: str,
|
| 27 |
+
api_base: str = "https://api.minimaxi.com/anthropic",
|
| 28 |
+
model: str = "MiniMax-M2.1",
|
| 29 |
+
retry_config: RetryConfig | None = None,
|
| 30 |
+
):
|
| 31 |
+
"""Initialize Anthropic client.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
api_key: API key for authentication
|
| 35 |
+
api_base: Base URL for the API (default: MiniMax Anthropic endpoint)
|
| 36 |
+
model: Model name to use (default: MiniMax-M2.1)
|
| 37 |
+
retry_config: Optional retry configuration
|
| 38 |
+
"""
|
| 39 |
+
super().__init__(api_key, api_base, model, retry_config)
|
| 40 |
+
|
| 41 |
+
# Initialize Anthropic async client
|
| 42 |
+
self.client = anthropic.AsyncAnthropic(
|
| 43 |
+
base_url=api_base,
|
| 44 |
+
api_key=api_key,
|
| 45 |
+
default_headers={"Authorization": f"Bearer {api_key}"},
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
async def _make_api_request(
|
| 49 |
+
self,
|
| 50 |
+
system_message: str | None,
|
| 51 |
+
api_messages: list[dict[str, Any]],
|
| 52 |
+
tools: list[Any] | None = None,
|
| 53 |
+
) -> anthropic.types.Message:
|
| 54 |
+
"""Execute API request (core method that can be retried).
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
system_message: Optional system message
|
| 58 |
+
api_messages: List of messages in Anthropic format
|
| 59 |
+
tools: Optional list of tools
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
Anthropic Message response
|
| 63 |
+
|
| 64 |
+
Raises:
|
| 65 |
+
Exception: API call failed
|
| 66 |
+
"""
|
| 67 |
+
params = {
|
| 68 |
+
"model": self.model,
|
| 69 |
+
"max_tokens": 16384,
|
| 70 |
+
"messages": api_messages,
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
if system_message:
|
| 74 |
+
params["system"] = system_message
|
| 75 |
+
|
| 76 |
+
if tools:
|
| 77 |
+
params["tools"] = self._convert_tools(tools)
|
| 78 |
+
|
| 79 |
+
# Use Anthropic SDK's async messages.create
|
| 80 |
+
response = await self.client.messages.create(**params)
|
| 81 |
+
return response
|
| 82 |
+
|
| 83 |
+
def _convert_tools(self, tools: list[Any]) -> list[dict[str, Any]]:
|
| 84 |
+
"""Convert tools to Anthropic format.
|
| 85 |
+
|
| 86 |
+
Anthropic tool format:
|
| 87 |
+
{
|
| 88 |
+
"name": "tool_name",
|
| 89 |
+
"description": "Tool description",
|
| 90 |
+
"input_schema": {
|
| 91 |
+
"type": "object",
|
| 92 |
+
"properties": {...},
|
| 93 |
+
"required": [...]
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
tools: List of Tool objects or dicts
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
List of tools in Anthropic dict format
|
| 102 |
+
"""
|
| 103 |
+
result = []
|
| 104 |
+
for tool in tools:
|
| 105 |
+
if isinstance(tool, dict):
|
| 106 |
+
result.append(tool)
|
| 107 |
+
elif hasattr(tool, "to_schema"):
|
| 108 |
+
# Tool object with to_schema method
|
| 109 |
+
result.append(tool.to_schema())
|
| 110 |
+
else:
|
| 111 |
+
raise TypeError(f"Unsupported tool type: {type(tool)}")
|
| 112 |
+
return result
|
| 113 |
+
|
| 114 |
+
def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]:
|
| 115 |
+
"""Convert internal messages to Anthropic format.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
messages: List of internal Message objects
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
Tuple of (system_message, api_messages)
|
| 122 |
+
"""
|
| 123 |
+
system_message = None
|
| 124 |
+
api_messages = []
|
| 125 |
+
|
| 126 |
+
for msg in messages:
|
| 127 |
+
if msg.role == "system":
|
| 128 |
+
system_message = msg.content
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
# For user and assistant messages
|
| 132 |
+
if msg.role in ["user", "assistant"]:
|
| 133 |
+
# Handle assistant messages with thinking or tool calls
|
| 134 |
+
if msg.role == "assistant" and (msg.thinking or msg.tool_calls):
|
| 135 |
+
# Build content blocks for assistant with thinking and/or tool calls
|
| 136 |
+
content_blocks = []
|
| 137 |
+
|
| 138 |
+
# Add thinking block if present
|
| 139 |
+
if msg.thinking:
|
| 140 |
+
content_blocks.append({"type": "thinking", "thinking": msg.thinking})
|
| 141 |
+
|
| 142 |
+
# Add text content if present
|
| 143 |
+
if msg.content:
|
| 144 |
+
content_blocks.append({"type": "text", "text": msg.content})
|
| 145 |
+
|
| 146 |
+
# Add tool use blocks
|
| 147 |
+
if msg.tool_calls:
|
| 148 |
+
for tool_call in msg.tool_calls:
|
| 149 |
+
content_blocks.append(
|
| 150 |
+
{
|
| 151 |
+
"type": "tool_use",
|
| 152 |
+
"id": tool_call.id,
|
| 153 |
+
"name": tool_call.function.name,
|
| 154 |
+
"input": tool_call.function.arguments,
|
| 155 |
+
}
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
api_messages.append({"role": "assistant", "content": content_blocks})
|
| 159 |
+
else:
|
| 160 |
+
api_messages.append({"role": msg.role, "content": msg.content})
|
| 161 |
+
|
| 162 |
+
# For tool result messages
|
| 163 |
+
elif msg.role == "tool":
|
| 164 |
+
# Anthropic uses user role with tool_result content blocks
|
| 165 |
+
api_messages.append(
|
| 166 |
+
{
|
| 167 |
+
"role": "user",
|
| 168 |
+
"content": [
|
| 169 |
+
{
|
| 170 |
+
"type": "tool_result",
|
| 171 |
+
"tool_use_id": msg.tool_call_id,
|
| 172 |
+
"content": msg.content,
|
| 173 |
+
}
|
| 174 |
+
],
|
| 175 |
+
}
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
return system_message, api_messages
|
| 179 |
+
|
| 180 |
+
def _prepare_request(
|
| 181 |
+
self,
|
| 182 |
+
messages: list[Message],
|
| 183 |
+
tools: list[Any] | None = None,
|
| 184 |
+
) -> dict[str, Any]:
|
| 185 |
+
"""Prepare the request for Anthropic API.
|
| 186 |
+
|
| 187 |
+
Args:
|
| 188 |
+
messages: List of conversation messages
|
| 189 |
+
tools: Optional list of available tools
|
| 190 |
+
|
| 191 |
+
Returns:
|
| 192 |
+
Dictionary containing request parameters
|
| 193 |
+
"""
|
| 194 |
+
system_message, api_messages = self._convert_messages(messages)
|
| 195 |
+
|
| 196 |
+
return {
|
| 197 |
+
"system_message": system_message,
|
| 198 |
+
"api_messages": api_messages,
|
| 199 |
+
"tools": tools,
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
def _parse_response(self, response: anthropic.types.Message) -> LLMResponse:
|
| 203 |
+
"""Parse Anthropic response into LLMResponse.
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
response: Anthropic Message response
|
| 207 |
+
|
| 208 |
+
Returns:
|
| 209 |
+
LLMResponse object
|
| 210 |
+
"""
|
| 211 |
+
# Extract text content, thinking, and tool calls
|
| 212 |
+
text_content = ""
|
| 213 |
+
thinking_content = ""
|
| 214 |
+
tool_calls = []
|
| 215 |
+
|
| 216 |
+
for block in response.content:
|
| 217 |
+
if block.type == "text":
|
| 218 |
+
text_content += block.text
|
| 219 |
+
elif block.type == "thinking":
|
| 220 |
+
thinking_content += block.thinking
|
| 221 |
+
elif block.type == "tool_use":
|
| 222 |
+
# Parse Anthropic tool_use block
|
| 223 |
+
tool_calls.append(
|
| 224 |
+
ToolCall(
|
| 225 |
+
id=block.id,
|
| 226 |
+
type="function",
|
| 227 |
+
function=FunctionCall(
|
| 228 |
+
name=block.name,
|
| 229 |
+
arguments=block.input,
|
| 230 |
+
),
|
| 231 |
+
)
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
# Extract token usage from response
|
| 235 |
+
# Anthropic usage includes: input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens
|
| 236 |
+
usage = None
|
| 237 |
+
if hasattr(response, "usage") and response.usage:
|
| 238 |
+
input_tokens = response.usage.input_tokens or 0
|
| 239 |
+
output_tokens = response.usage.output_tokens or 0
|
| 240 |
+
cache_read_tokens = getattr(response.usage, "cache_read_input_tokens", 0) or 0
|
| 241 |
+
cache_creation_tokens = getattr(response.usage, "cache_creation_input_tokens", 0) or 0
|
| 242 |
+
total_input_tokens = input_tokens + cache_read_tokens + cache_creation_tokens
|
| 243 |
+
usage = TokenUsage(
|
| 244 |
+
prompt_tokens=total_input_tokens,
|
| 245 |
+
completion_tokens=output_tokens,
|
| 246 |
+
total_tokens=total_input_tokens + output_tokens,
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
return LLMResponse(
|
| 250 |
+
content=text_content,
|
| 251 |
+
thinking=thinking_content if thinking_content else None,
|
| 252 |
+
tool_calls=tool_calls if tool_calls else None,
|
| 253 |
+
finish_reason=response.stop_reason or "stop",
|
| 254 |
+
usage=usage,
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
async def generate(
|
| 258 |
+
self,
|
| 259 |
+
messages: list[Message],
|
| 260 |
+
tools: list[Any] | None = None,
|
| 261 |
+
) -> LLMResponse:
|
| 262 |
+
"""Generate response from Anthropic LLM.
|
| 263 |
+
|
| 264 |
+
Args:
|
| 265 |
+
messages: List of conversation messages
|
| 266 |
+
tools: Optional list of available tools
|
| 267 |
+
|
| 268 |
+
Returns:
|
| 269 |
+
LLMResponse containing the generated content
|
| 270 |
+
"""
|
| 271 |
+
# Prepare request
|
| 272 |
+
request_params = self._prepare_request(messages, tools)
|
| 273 |
+
|
| 274 |
+
# Make API request with retry logic
|
| 275 |
+
if self.retry_config.enabled:
|
| 276 |
+
# Apply retry logic
|
| 277 |
+
retry_decorator = async_retry(config=self.retry_config, on_retry=self.retry_callback)
|
| 278 |
+
api_call = retry_decorator(self._make_api_request)
|
| 279 |
+
response = await api_call(
|
| 280 |
+
request_params["system_message"],
|
| 281 |
+
request_params["api_messages"],
|
| 282 |
+
request_params["tools"],
|
| 283 |
+
)
|
| 284 |
+
else:
|
| 285 |
+
# Don't use retry
|
| 286 |
+
response = await self._make_api_request(
|
| 287 |
+
request_params["system_message"],
|
| 288 |
+
request_params["api_messages"],
|
| 289 |
+
request_params["tools"],
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
# Parse and return response
|
| 293 |
+
return self._parse_response(response)
|
mini_agent/llm/base.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Base class for LLM clients."""
|
| 2 |
+
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
from ..retry import RetryConfig
|
| 7 |
+
from ..schema import LLMResponse, Message
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class LLMClientBase(ABC):
|
| 11 |
+
"""Abstract base class for LLM clients.
|
| 12 |
+
|
| 13 |
+
This class defines the interface that all LLM clients must implement,
|
| 14 |
+
regardless of the underlying API protocol (Anthropic, OpenAI, etc.).
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
api_key: str,
|
| 20 |
+
api_base: str,
|
| 21 |
+
model: str,
|
| 22 |
+
retry_config: RetryConfig | None = None,
|
| 23 |
+
):
|
| 24 |
+
"""Initialize the LLM client.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
api_key: API key for authentication
|
| 28 |
+
api_base: Base URL for the API
|
| 29 |
+
model: Model name to use
|
| 30 |
+
retry_config: Optional retry configuration
|
| 31 |
+
"""
|
| 32 |
+
self.api_key = api_key
|
| 33 |
+
self.api_base = api_base
|
| 34 |
+
self.model = model
|
| 35 |
+
self.retry_config = retry_config or RetryConfig()
|
| 36 |
+
|
| 37 |
+
# Callback for tracking retry count
|
| 38 |
+
self.retry_callback = None
|
| 39 |
+
|
| 40 |
+
@abstractmethod
|
| 41 |
+
async def generate(
|
| 42 |
+
self,
|
| 43 |
+
messages: list[Message],
|
| 44 |
+
tools: list[Any] | None = None,
|
| 45 |
+
) -> LLMResponse:
|
| 46 |
+
"""Generate response from LLM.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
messages: List of conversation messages
|
| 50 |
+
tools: Optional list of Tool objects or dicts
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
LLMResponse containing the generated content, thinking, and tool calls
|
| 54 |
+
"""
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
@abstractmethod
|
| 58 |
+
def _prepare_request(
|
| 59 |
+
self,
|
| 60 |
+
messages: list[Message],
|
| 61 |
+
tools: list[Any] | None = None,
|
| 62 |
+
) -> dict[str, Any]:
|
| 63 |
+
"""Prepare the request payload for the API.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
messages: List of conversation messages
|
| 67 |
+
tools: Optional list of available tools
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
Dictionary containing the request payload
|
| 71 |
+
"""
|
| 72 |
+
pass
|
| 73 |
+
|
| 74 |
+
@abstractmethod
|
| 75 |
+
def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]:
|
| 76 |
+
"""Convert internal message format to API-specific format.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
messages: List of internal Message objects
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
Tuple of (system_message, api_messages)
|
| 83 |
+
"""
|
| 84 |
+
pass
|
mini_agent/llm/llm_wrapper.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LLM client wrapper that supports multiple providers.
|
| 2 |
+
|
| 3 |
+
This module provides a unified interface for different LLM providers
|
| 4 |
+
(Anthropic and OpenAI) through a single LLMClient class.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
from ..retry import RetryConfig
|
| 10 |
+
from ..schema import LLMProvider, LLMResponse, Message
|
| 11 |
+
from .anthropic_client import AnthropicClient
|
| 12 |
+
from .base import LLMClientBase
|
| 13 |
+
from .openai_client import OpenAIClient
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class LLMClient:
|
| 19 |
+
"""LLM Client wrapper supporting multiple providers.
|
| 20 |
+
|
| 21 |
+
This class provides a unified interface for different LLM providers.
|
| 22 |
+
It automatically instantiates the correct underlying client based on
|
| 23 |
+
the provider parameter.
|
| 24 |
+
|
| 25 |
+
For MiniMax API (api.minimax.io or api.minimaxi.com), it appends the
|
| 26 |
+
appropriate endpoint suffix based on provider:
|
| 27 |
+
- anthropic: /anthropic
|
| 28 |
+
- openai: /v1
|
| 29 |
+
|
| 30 |
+
For third-party APIs, it uses the api_base as-is.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
# MiniMax API domains that need automatic suffix handling
|
| 34 |
+
MINIMAX_DOMAINS = ("api.minimax.io", "api.minimaxi.com")
|
| 35 |
+
|
| 36 |
+
def __init__(
|
| 37 |
+
self,
|
| 38 |
+
api_key: str,
|
| 39 |
+
provider: LLMProvider = LLMProvider.ANTHROPIC,
|
| 40 |
+
api_base: str = "https://api.minimaxi.com",
|
| 41 |
+
model: str = "MiniMax-M2.1",
|
| 42 |
+
retry_config: RetryConfig | None = None,
|
| 43 |
+
):
|
| 44 |
+
"""Initialize LLM client with specified provider.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
api_key: API key for authentication
|
| 48 |
+
provider: LLM provider (anthropic or openai)
|
| 49 |
+
api_base: Base URL for the API (default: https://api.minimaxi.com)
|
| 50 |
+
For MiniMax API, suffix is auto-appended based on provider.
|
| 51 |
+
For third-party APIs (e.g., https://api.siliconflow.cn/v1), used as-is.
|
| 52 |
+
model: Model name to use
|
| 53 |
+
retry_config: Optional retry configuration
|
| 54 |
+
"""
|
| 55 |
+
self.provider = provider
|
| 56 |
+
self.api_key = api_key
|
| 57 |
+
self.model = model
|
| 58 |
+
self.retry_config = retry_config or RetryConfig()
|
| 59 |
+
|
| 60 |
+
# Normalize api_base (remove trailing slash)
|
| 61 |
+
api_base = api_base.rstrip("/")
|
| 62 |
+
|
| 63 |
+
# Check if this is a MiniMax API endpoint
|
| 64 |
+
is_minimax = any(domain in api_base for domain in self.MINIMAX_DOMAINS)
|
| 65 |
+
|
| 66 |
+
if is_minimax:
|
| 67 |
+
# For MiniMax API, ensure correct suffix based on provider
|
| 68 |
+
# Strip any existing suffix first
|
| 69 |
+
api_base = api_base.replace("/anthropic", "").replace("/v1", "")
|
| 70 |
+
if provider == LLMProvider.ANTHROPIC:
|
| 71 |
+
full_api_base = f"{api_base}/anthropic"
|
| 72 |
+
elif provider == LLMProvider.OPENAI:
|
| 73 |
+
full_api_base = f"{api_base}/v1"
|
| 74 |
+
else:
|
| 75 |
+
raise ValueError(f"Unsupported provider: {provider}")
|
| 76 |
+
else:
|
| 77 |
+
# For third-party APIs, use api_base as-is
|
| 78 |
+
full_api_base = api_base
|
| 79 |
+
|
| 80 |
+
self.api_base = full_api_base
|
| 81 |
+
|
| 82 |
+
# Instantiate the appropriate client
|
| 83 |
+
self._client: LLMClientBase
|
| 84 |
+
if provider == LLMProvider.ANTHROPIC:
|
| 85 |
+
self._client = AnthropicClient(
|
| 86 |
+
api_key=api_key,
|
| 87 |
+
api_base=full_api_base,
|
| 88 |
+
model=model,
|
| 89 |
+
retry_config=retry_config,
|
| 90 |
+
)
|
| 91 |
+
elif provider == LLMProvider.OPENAI:
|
| 92 |
+
self._client = OpenAIClient(
|
| 93 |
+
api_key=api_key,
|
| 94 |
+
api_base=full_api_base,
|
| 95 |
+
model=model,
|
| 96 |
+
retry_config=retry_config,
|
| 97 |
+
)
|
| 98 |
+
else:
|
| 99 |
+
raise ValueError(f"Unsupported provider: {provider}")
|
| 100 |
+
|
| 101 |
+
logger.info("Initialized LLM client with provider: %s, api_base: %s", provider, full_api_base)
|
| 102 |
+
|
| 103 |
+
@property
|
| 104 |
+
def retry_callback(self):
|
| 105 |
+
"""Get retry callback."""
|
| 106 |
+
return self._client.retry_callback
|
| 107 |
+
|
| 108 |
+
@retry_callback.setter
|
| 109 |
+
def retry_callback(self, value):
|
| 110 |
+
"""Set retry callback."""
|
| 111 |
+
self._client.retry_callback = value
|
| 112 |
+
|
| 113 |
+
async def generate(
|
| 114 |
+
self,
|
| 115 |
+
messages: list[Message],
|
| 116 |
+
tools: list | None = None,
|
| 117 |
+
) -> LLMResponse:
|
| 118 |
+
"""Generate response from LLM.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
messages: List of conversation messages
|
| 122 |
+
tools: Optional list of Tool objects or dicts
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
LLMResponse containing the generated content
|
| 126 |
+
"""
|
| 127 |
+
return await self._client.generate(messages, tools)
|
mini_agent/llm/openai_client.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenAI LLM client implementation."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from openai import AsyncOpenAI
|
| 8 |
+
|
| 9 |
+
from ..retry import RetryConfig, async_retry
|
| 10 |
+
from ..schema import FunctionCall, LLMResponse, Message, TokenUsage, ToolCall
|
| 11 |
+
from .base import LLMClientBase
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class OpenAIClient(LLMClientBase):
|
| 17 |
+
"""LLM client using OpenAI's protocol.
|
| 18 |
+
|
| 19 |
+
This client uses the official OpenAI SDK and supports:
|
| 20 |
+
- Reasoning content (via reasoning_split=True)
|
| 21 |
+
- Tool calling
|
| 22 |
+
- Retry logic
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(
|
| 26 |
+
self,
|
| 27 |
+
api_key: str,
|
| 28 |
+
api_base: str = "https://api.minimaxi.com/v1",
|
| 29 |
+
model: str = "MiniMax-M2.1",
|
| 30 |
+
retry_config: RetryConfig | None = None,
|
| 31 |
+
):
|
| 32 |
+
"""Initialize OpenAI client.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
api_key: API key for authentication
|
| 36 |
+
api_base: Base URL for the API (default: MiniMax OpenAI endpoint)
|
| 37 |
+
model: Model name to use (default: MiniMax-M2.1)
|
| 38 |
+
retry_config: Optional retry configuration
|
| 39 |
+
"""
|
| 40 |
+
super().__init__(api_key, api_base, model, retry_config)
|
| 41 |
+
|
| 42 |
+
# Initialize OpenAI client
|
| 43 |
+
self.client = AsyncOpenAI(
|
| 44 |
+
api_key=api_key,
|
| 45 |
+
base_url=api_base,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
async def _make_api_request(
|
| 49 |
+
self,
|
| 50 |
+
api_messages: list[dict[str, Any]],
|
| 51 |
+
tools: list[Any] | None = None,
|
| 52 |
+
) -> Any:
|
| 53 |
+
"""Execute API request (core method that can be retried).
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
api_messages: List of messages in OpenAI format
|
| 57 |
+
tools: Optional list of tools
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
OpenAI ChatCompletion response (full response including usage)
|
| 61 |
+
|
| 62 |
+
Raises:
|
| 63 |
+
Exception: API call failed
|
| 64 |
+
"""
|
| 65 |
+
params = {
|
| 66 |
+
"model": self.model,
|
| 67 |
+
"messages": api_messages,
|
| 68 |
+
# Enable reasoning_split to separate thinking content
|
| 69 |
+
"extra_body": {"reasoning_split": True},
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
if tools:
|
| 73 |
+
params["tools"] = self._convert_tools(tools)
|
| 74 |
+
|
| 75 |
+
# Use OpenAI SDK's chat.completions.create
|
| 76 |
+
response = await self.client.chat.completions.create(**params)
|
| 77 |
+
# Return full response to access usage info
|
| 78 |
+
return response
|
| 79 |
+
|
| 80 |
+
def _convert_tools(self, tools: list[Any]) -> list[dict[str, Any]]:
|
| 81 |
+
"""Convert tools to OpenAI format.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
tools: List of Tool objects or dicts
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
List of tools in OpenAI dict format
|
| 88 |
+
"""
|
| 89 |
+
result = []
|
| 90 |
+
for tool in tools:
|
| 91 |
+
if isinstance(tool, dict):
|
| 92 |
+
# If already a dict, check if it's in OpenAI format
|
| 93 |
+
if "type" in tool and tool["type"] == "function":
|
| 94 |
+
result.append(tool)
|
| 95 |
+
else:
|
| 96 |
+
# Assume it's in Anthropic format, convert to OpenAI
|
| 97 |
+
result.append(
|
| 98 |
+
{
|
| 99 |
+
"type": "function",
|
| 100 |
+
"function": {
|
| 101 |
+
"name": tool["name"],
|
| 102 |
+
"description": tool["description"],
|
| 103 |
+
"parameters": tool["input_schema"],
|
| 104 |
+
},
|
| 105 |
+
}
|
| 106 |
+
)
|
| 107 |
+
elif hasattr(tool, "to_openai_schema"):
|
| 108 |
+
# Tool object with to_openai_schema method
|
| 109 |
+
result.append(tool.to_openai_schema())
|
| 110 |
+
else:
|
| 111 |
+
raise TypeError(f"Unsupported tool type: {type(tool)}")
|
| 112 |
+
return result
|
| 113 |
+
|
| 114 |
+
def _convert_messages(self, messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]:
|
| 115 |
+
"""Convert internal messages to OpenAI format.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
messages: List of internal Message objects
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
Tuple of (system_message, api_messages)
|
| 122 |
+
Note: OpenAI includes system message in the messages array
|
| 123 |
+
"""
|
| 124 |
+
api_messages = []
|
| 125 |
+
|
| 126 |
+
for msg in messages:
|
| 127 |
+
if msg.role == "system":
|
| 128 |
+
# OpenAI includes system message in messages array
|
| 129 |
+
api_messages.append({"role": "system", "content": msg.content})
|
| 130 |
+
continue
|
| 131 |
+
|
| 132 |
+
# For user messages
|
| 133 |
+
if msg.role == "user":
|
| 134 |
+
api_messages.append({"role": "user", "content": msg.content})
|
| 135 |
+
|
| 136 |
+
# For assistant messages
|
| 137 |
+
elif msg.role == "assistant":
|
| 138 |
+
assistant_msg = {"role": "assistant"}
|
| 139 |
+
|
| 140 |
+
# Add content if present
|
| 141 |
+
if msg.content:
|
| 142 |
+
assistant_msg["content"] = msg.content
|
| 143 |
+
|
| 144 |
+
# Add tool calls if present
|
| 145 |
+
if msg.tool_calls:
|
| 146 |
+
tool_calls_list = []
|
| 147 |
+
for tool_call in msg.tool_calls:
|
| 148 |
+
tool_calls_list.append(
|
| 149 |
+
{
|
| 150 |
+
"id": tool_call.id,
|
| 151 |
+
"type": "function",
|
| 152 |
+
"function": {
|
| 153 |
+
"name": tool_call.function.name,
|
| 154 |
+
"arguments": json.dumps(tool_call.function.arguments),
|
| 155 |
+
},
|
| 156 |
+
}
|
| 157 |
+
)
|
| 158 |
+
assistant_msg["tool_calls"] = tool_calls_list
|
| 159 |
+
|
| 160 |
+
# IMPORTANT: Add reasoning_details if thinking is present
|
| 161 |
+
# This is CRITICAL for Interleaved Thinking to work properly!
|
| 162 |
+
# The complete response_message (including reasoning_details) must be
|
| 163 |
+
# preserved in Message History and passed back to the model in the next turn.
|
| 164 |
+
# This ensures the model's chain of thought is not interrupted.
|
| 165 |
+
if msg.thinking:
|
| 166 |
+
assistant_msg["reasoning_details"] = [{"text": msg.thinking}]
|
| 167 |
+
|
| 168 |
+
api_messages.append(assistant_msg)
|
| 169 |
+
|
| 170 |
+
# For tool result messages
|
| 171 |
+
elif msg.role == "tool":
|
| 172 |
+
api_messages.append(
|
| 173 |
+
{
|
| 174 |
+
"role": "tool",
|
| 175 |
+
"tool_call_id": msg.tool_call_id,
|
| 176 |
+
"content": msg.content,
|
| 177 |
+
}
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
return None, api_messages
|
| 181 |
+
|
| 182 |
+
def _prepare_request(
|
| 183 |
+
self,
|
| 184 |
+
messages: list[Message],
|
| 185 |
+
tools: list[Any] | None = None,
|
| 186 |
+
) -> dict[str, Any]:
|
| 187 |
+
"""Prepare the request for OpenAI API.
|
| 188 |
+
|
| 189 |
+
Args:
|
| 190 |
+
messages: List of conversation messages
|
| 191 |
+
tools: Optional list of available tools
|
| 192 |
+
|
| 193 |
+
Returns:
|
| 194 |
+
Dictionary containing request parameters
|
| 195 |
+
"""
|
| 196 |
+
_, api_messages = self._convert_messages(messages)
|
| 197 |
+
|
| 198 |
+
return {
|
| 199 |
+
"api_messages": api_messages,
|
| 200 |
+
"tools": tools,
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
def _parse_response(self, response: Any) -> LLMResponse:
|
| 204 |
+
"""Parse OpenAI response into LLMResponse.
|
| 205 |
+
|
| 206 |
+
Args:
|
| 207 |
+
response: OpenAI ChatCompletion response (full response object)
|
| 208 |
+
|
| 209 |
+
Returns:
|
| 210 |
+
LLMResponse object
|
| 211 |
+
"""
|
| 212 |
+
# Get message from response
|
| 213 |
+
message = response.choices[0].message
|
| 214 |
+
|
| 215 |
+
# Extract text content
|
| 216 |
+
text_content = message.content or ""
|
| 217 |
+
|
| 218 |
+
# Extract thinking content from reasoning_details
|
| 219 |
+
thinking_content = ""
|
| 220 |
+
if hasattr(message, "reasoning_details") and message.reasoning_details:
|
| 221 |
+
# reasoning_details is a list of reasoning blocks
|
| 222 |
+
for detail in message.reasoning_details:
|
| 223 |
+
if hasattr(detail, "text"):
|
| 224 |
+
thinking_content += detail.text
|
| 225 |
+
|
| 226 |
+
# Extract tool calls
|
| 227 |
+
tool_calls = []
|
| 228 |
+
if message.tool_calls:
|
| 229 |
+
for tool_call in message.tool_calls:
|
| 230 |
+
# Parse arguments from JSON string
|
| 231 |
+
arguments = json.loads(tool_call.function.arguments)
|
| 232 |
+
|
| 233 |
+
tool_calls.append(
|
| 234 |
+
ToolCall(
|
| 235 |
+
id=tool_call.id,
|
| 236 |
+
type="function",
|
| 237 |
+
function=FunctionCall(
|
| 238 |
+
name=tool_call.function.name,
|
| 239 |
+
arguments=arguments,
|
| 240 |
+
),
|
| 241 |
+
)
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
# Extract token usage from response
|
| 245 |
+
usage = None
|
| 246 |
+
if hasattr(response, "usage") and response.usage:
|
| 247 |
+
usage = TokenUsage(
|
| 248 |
+
prompt_tokens=response.usage.prompt_tokens or 0,
|
| 249 |
+
completion_tokens=response.usage.completion_tokens or 0,
|
| 250 |
+
total_tokens=response.usage.total_tokens or 0,
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
return LLMResponse(
|
| 254 |
+
content=text_content,
|
| 255 |
+
thinking=thinking_content if thinking_content else None,
|
| 256 |
+
tool_calls=tool_calls if tool_calls else None,
|
| 257 |
+
finish_reason="stop", # OpenAI doesn't provide finish_reason in the message
|
| 258 |
+
usage=usage,
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
async def generate(
|
| 262 |
+
self,
|
| 263 |
+
messages: list[Message],
|
| 264 |
+
tools: list[Any] | None = None,
|
| 265 |
+
) -> LLMResponse:
|
| 266 |
+
"""Generate response from OpenAI LLM.
|
| 267 |
+
|
| 268 |
+
Args:
|
| 269 |
+
messages: List of conversation messages
|
| 270 |
+
tools: Optional list of available tools
|
| 271 |
+
|
| 272 |
+
Returns:
|
| 273 |
+
LLMResponse containing the generated content
|
| 274 |
+
"""
|
| 275 |
+
# Prepare request
|
| 276 |
+
request_params = self._prepare_request(messages, tools)
|
| 277 |
+
|
| 278 |
+
# Make API request with retry logic
|
| 279 |
+
if self.retry_config.enabled:
|
| 280 |
+
# Apply retry logic
|
| 281 |
+
retry_decorator = async_retry(config=self.retry_config, on_retry=self.retry_callback)
|
| 282 |
+
api_call = retry_decorator(self._make_api_request)
|
| 283 |
+
response = await api_call(
|
| 284 |
+
request_params["api_messages"],
|
| 285 |
+
request_params["tools"],
|
| 286 |
+
)
|
| 287 |
+
else:
|
| 288 |
+
# Don't use retry
|
| 289 |
+
response = await self._make_api_request(
|
| 290 |
+
request_params["api_messages"],
|
| 291 |
+
request_params["tools"],
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# Parse and return response
|
| 295 |
+
return self._parse_response(response)
|
mini_agent/logger.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Agent run logger"""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from .schema import Message, ToolCall
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class AgentLogger:
|
| 12 |
+
"""Agent run logger
|
| 13 |
+
|
| 14 |
+
Responsible for recording the complete interaction process of each agent run, including:
|
| 15 |
+
- LLM requests and responses
|
| 16 |
+
- Tool calls and results
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def __init__(self):
|
| 20 |
+
"""Initialize logger
|
| 21 |
+
|
| 22 |
+
Logs are stored in ~/.mini-agent/log/ directory
|
| 23 |
+
"""
|
| 24 |
+
# Use ~/.mini-agent/log/ directory for logs
|
| 25 |
+
self.log_dir = Path.home() / ".mini-agent" / "log"
|
| 26 |
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
| 27 |
+
self.log_file = None
|
| 28 |
+
self.log_index = 0
|
| 29 |
+
|
| 30 |
+
def start_new_run(self):
|
| 31 |
+
"""Start new run, create new log file"""
|
| 32 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 33 |
+
log_filename = f"agent_run_{timestamp}.log"
|
| 34 |
+
self.log_file = self.log_dir / log_filename
|
| 35 |
+
self.log_index = 0
|
| 36 |
+
|
| 37 |
+
# Write log header
|
| 38 |
+
with open(self.log_file, "w", encoding="utf-8") as f:
|
| 39 |
+
f.write("=" * 80 + "\n")
|
| 40 |
+
f.write(f"Agent Run Log - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
| 41 |
+
f.write("=" * 80 + "\n\n")
|
| 42 |
+
|
| 43 |
+
def log_request(self, messages: list[Message], tools: list[Any] | None = None):
|
| 44 |
+
"""Log LLM request
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
messages: Message list
|
| 48 |
+
tools: Tool list (optional)
|
| 49 |
+
"""
|
| 50 |
+
self.log_index += 1
|
| 51 |
+
|
| 52 |
+
# Build complete request data structure
|
| 53 |
+
request_data = {
|
| 54 |
+
"messages": [],
|
| 55 |
+
"tools": [],
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
# Convert messages to JSON serializable format
|
| 59 |
+
for msg in messages:
|
| 60 |
+
msg_dict = {
|
| 61 |
+
"role": msg.role,
|
| 62 |
+
"content": msg.content,
|
| 63 |
+
}
|
| 64 |
+
if msg.thinking:
|
| 65 |
+
msg_dict["thinking"] = msg.thinking
|
| 66 |
+
if msg.tool_calls:
|
| 67 |
+
msg_dict["tool_calls"] = [tc.model_dump() for tc in msg.tool_calls]
|
| 68 |
+
if msg.tool_call_id:
|
| 69 |
+
msg_dict["tool_call_id"] = msg.tool_call_id
|
| 70 |
+
if msg.name:
|
| 71 |
+
msg_dict["name"] = msg.name
|
| 72 |
+
|
| 73 |
+
request_data["messages"].append(msg_dict)
|
| 74 |
+
|
| 75 |
+
# Only record tool names
|
| 76 |
+
if tools:
|
| 77 |
+
request_data["tools"] = [tool.name for tool in tools]
|
| 78 |
+
|
| 79 |
+
# Format as JSON
|
| 80 |
+
content = "LLM Request:\n\n"
|
| 81 |
+
content += json.dumps(request_data, indent=2, ensure_ascii=False)
|
| 82 |
+
|
| 83 |
+
self._write_log("REQUEST", content)
|
| 84 |
+
|
| 85 |
+
def log_response(
|
| 86 |
+
self,
|
| 87 |
+
content: str,
|
| 88 |
+
thinking: str | None = None,
|
| 89 |
+
tool_calls: list[ToolCall] | None = None,
|
| 90 |
+
finish_reason: str | None = None,
|
| 91 |
+
):
|
| 92 |
+
"""Log LLM response
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
content: Response content
|
| 96 |
+
thinking: Thinking content (optional)
|
| 97 |
+
tool_calls: Tool call list (optional)
|
| 98 |
+
finish_reason: Finish reason (optional)
|
| 99 |
+
"""
|
| 100 |
+
self.log_index += 1
|
| 101 |
+
|
| 102 |
+
# Build complete response data structure
|
| 103 |
+
response_data = {
|
| 104 |
+
"content": content,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
if thinking:
|
| 108 |
+
response_data["thinking"] = thinking
|
| 109 |
+
|
| 110 |
+
if tool_calls:
|
| 111 |
+
response_data["tool_calls"] = [tc.model_dump() for tc in tool_calls]
|
| 112 |
+
|
| 113 |
+
if finish_reason:
|
| 114 |
+
response_data["finish_reason"] = finish_reason
|
| 115 |
+
|
| 116 |
+
# Format as JSON
|
| 117 |
+
log_content = "LLM Response:\n\n"
|
| 118 |
+
log_content += json.dumps(response_data, indent=2, ensure_ascii=False)
|
| 119 |
+
|
| 120 |
+
self._write_log("RESPONSE", log_content)
|
| 121 |
+
|
| 122 |
+
def log_tool_result(
|
| 123 |
+
self,
|
| 124 |
+
tool_name: str,
|
| 125 |
+
arguments: dict[str, Any],
|
| 126 |
+
result_success: bool,
|
| 127 |
+
result_content: str | None = None,
|
| 128 |
+
result_error: str | None = None,
|
| 129 |
+
):
|
| 130 |
+
"""Log tool execution result
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
tool_name: Tool name
|
| 134 |
+
arguments: Tool arguments
|
| 135 |
+
result_success: Whether successful
|
| 136 |
+
result_content: Result content (on success)
|
| 137 |
+
result_error: Error message (on failure)
|
| 138 |
+
"""
|
| 139 |
+
self.log_index += 1
|
| 140 |
+
|
| 141 |
+
# Build complete tool execution result data structure
|
| 142 |
+
tool_result_data = {
|
| 143 |
+
"tool_name": tool_name,
|
| 144 |
+
"arguments": arguments,
|
| 145 |
+
"success": result_success,
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
if result_success:
|
| 149 |
+
tool_result_data["result"] = result_content
|
| 150 |
+
else:
|
| 151 |
+
tool_result_data["error"] = result_error
|
| 152 |
+
|
| 153 |
+
# Format as JSON
|
| 154 |
+
content = "Tool Execution:\n\n"
|
| 155 |
+
content += json.dumps(tool_result_data, indent=2, ensure_ascii=False)
|
| 156 |
+
|
| 157 |
+
self._write_log("TOOL_RESULT", content)
|
| 158 |
+
|
| 159 |
+
def _write_log(self, log_type: str, content: str):
|
| 160 |
+
"""Write log entry
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
log_type: Log type (REQUEST, RESPONSE, TOOL_RESULT)
|
| 164 |
+
content: Log content
|
| 165 |
+
"""
|
| 166 |
+
if self.log_file is None:
|
| 167 |
+
return
|
| 168 |
+
|
| 169 |
+
with open(self.log_file, "a", encoding="utf-8") as f:
|
| 170 |
+
f.write("\n" + "-" * 80 + "\n")
|
| 171 |
+
f.write(f"[{self.log_index}] {log_type}\n")
|
| 172 |
+
f.write(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}\n")
|
| 173 |
+
f.write("-" * 80 + "\n")
|
| 174 |
+
f.write(content + "\n")
|
| 175 |
+
|
| 176 |
+
def get_log_file_path(self) -> Path:
|
| 177 |
+
"""Get current log file path"""
|
| 178 |
+
return self.log_file
|
mini_agent/retry.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Elegant retry mechanism module
|
| 2 |
+
|
| 3 |
+
Provides decorators and utility functions to support retry logic for async functions.
|
| 4 |
+
|
| 5 |
+
Features:
|
| 6 |
+
- Supports exponential backoff strategy
|
| 7 |
+
- Configurable retry count and intervals
|
| 8 |
+
- Supports specifying retryable exception types
|
| 9 |
+
- Detailed logging
|
| 10 |
+
- Fully decoupled, non-invasive to business code
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import asyncio
|
| 14 |
+
import functools
|
| 15 |
+
import logging
|
| 16 |
+
from typing import Any, Callable, Type, TypeVar
|
| 17 |
+
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
T = TypeVar("T")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class RetryConfig:
|
| 24 |
+
"""Retry configuration class"""
|
| 25 |
+
|
| 26 |
+
def __init__(
|
| 27 |
+
self,
|
| 28 |
+
enabled: bool = True,
|
| 29 |
+
max_retries: int = 3,
|
| 30 |
+
initial_delay: float = 1.0,
|
| 31 |
+
max_delay: float = 60.0,
|
| 32 |
+
exponential_base: float = 2.0,
|
| 33 |
+
retryable_exceptions: tuple[Type[Exception], ...] = (Exception,),
|
| 34 |
+
):
|
| 35 |
+
"""
|
| 36 |
+
Args:
|
| 37 |
+
enabled: Whether to enable retry mechanism
|
| 38 |
+
max_retries: Maximum number of retries
|
| 39 |
+
initial_delay: Initial delay time (seconds)
|
| 40 |
+
max_delay: Maximum delay time (seconds)
|
| 41 |
+
exponential_base: Exponential backoff base
|
| 42 |
+
retryable_exceptions: Tuple of retryable exception types
|
| 43 |
+
"""
|
| 44 |
+
self.enabled = enabled
|
| 45 |
+
self.max_retries = max_retries
|
| 46 |
+
self.initial_delay = initial_delay
|
| 47 |
+
self.max_delay = max_delay
|
| 48 |
+
self.exponential_base = exponential_base
|
| 49 |
+
self.retryable_exceptions = retryable_exceptions
|
| 50 |
+
|
| 51 |
+
def calculate_delay(self, attempt: int) -> float:
|
| 52 |
+
"""Calculate delay time (exponential backoff)
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
attempt: Current attempt number (starting from 0)
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
Delay time (seconds)
|
| 59 |
+
"""
|
| 60 |
+
delay = self.initial_delay * (self.exponential_base**attempt)
|
| 61 |
+
return min(delay, self.max_delay)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class RetryExhaustedError(Exception):
|
| 65 |
+
"""Retry exhausted exception"""
|
| 66 |
+
|
| 67 |
+
def __init__(self, last_exception: Exception, attempts: int):
|
| 68 |
+
self.last_exception = last_exception
|
| 69 |
+
self.attempts = attempts
|
| 70 |
+
super().__init__(f"Retry failed after {attempts} attempts. Last error: {str(last_exception)}")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def async_retry(
|
| 74 |
+
config: RetryConfig | None = None,
|
| 75 |
+
on_retry: Callable[[Exception, int], None] | None = None,
|
| 76 |
+
) -> Callable:
|
| 77 |
+
"""Async function retry decorator
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
config: Retry configuration object, uses default config if None
|
| 81 |
+
on_retry: Callback function on retry, receives exception and current attempt number
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
Decorator function
|
| 85 |
+
|
| 86 |
+
Example:
|
| 87 |
+
```python
|
| 88 |
+
@async_retry(RetryConfig(max_retries=3, initial_delay=1.0))
|
| 89 |
+
async def call_api():
|
| 90 |
+
# API call code
|
| 91 |
+
pass
|
| 92 |
+
```
|
| 93 |
+
"""
|
| 94 |
+
if config is None:
|
| 95 |
+
config = RetryConfig()
|
| 96 |
+
|
| 97 |
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
| 98 |
+
@functools.wraps(func)
|
| 99 |
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
| 100 |
+
last_exception: Exception | None = None
|
| 101 |
+
|
| 102 |
+
for attempt in range(config.max_retries + 1):
|
| 103 |
+
try:
|
| 104 |
+
# Try to execute function
|
| 105 |
+
return await func(*args, **kwargs)
|
| 106 |
+
|
| 107 |
+
except config.retryable_exceptions as e:
|
| 108 |
+
last_exception = e
|
| 109 |
+
|
| 110 |
+
# If this is the last attempt, don't retry
|
| 111 |
+
if attempt >= config.max_retries:
|
| 112 |
+
logger.error(f"Function {func.__name__} retry failed, reached maximum retry count {config.max_retries}")
|
| 113 |
+
raise RetryExhaustedError(e, attempt + 1)
|
| 114 |
+
|
| 115 |
+
# Calculate delay time
|
| 116 |
+
delay = config.calculate_delay(attempt)
|
| 117 |
+
|
| 118 |
+
# Log
|
| 119 |
+
logger.warning(
|
| 120 |
+
f"Function {func.__name__} call {attempt + 1} failed: {str(e)}, "
|
| 121 |
+
f"retrying attempt {attempt + 2} after {delay:.2f} seconds"
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
# Call callback function
|
| 125 |
+
if on_retry:
|
| 126 |
+
on_retry(e, attempt + 1)
|
| 127 |
+
|
| 128 |
+
# Wait before retry
|
| 129 |
+
await asyncio.sleep(delay)
|
| 130 |
+
|
| 131 |
+
# Should not reach here in theory
|
| 132 |
+
if last_exception:
|
| 133 |
+
raise last_exception
|
| 134 |
+
raise Exception("Unknown error")
|
| 135 |
+
|
| 136 |
+
return wrapper
|
| 137 |
+
|
| 138 |
+
return decorator
|
mini_agent/schema/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Schema definitions for Mini-Agent."""
|
| 2 |
+
|
| 3 |
+
from .schema import (
|
| 4 |
+
FunctionCall,
|
| 5 |
+
LLMProvider,
|
| 6 |
+
LLMResponse,
|
| 7 |
+
Message,
|
| 8 |
+
TokenUsage,
|
| 9 |
+
ToolCall,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"FunctionCall",
|
| 14 |
+
"LLMProvider",
|
| 15 |
+
"LLMResponse",
|
| 16 |
+
"Message",
|
| 17 |
+
"TokenUsage",
|
| 18 |
+
"ToolCall",
|
| 19 |
+
]
|
mini_agent/schema/schema.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
from typing import Any
|
| 3 |
+
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class LLMProvider(str, Enum):
|
| 8 |
+
"""LLM provider types."""
|
| 9 |
+
|
| 10 |
+
ANTHROPIC = "anthropic"
|
| 11 |
+
OPENAI = "openai"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class FunctionCall(BaseModel):
|
| 15 |
+
"""Function call details."""
|
| 16 |
+
|
| 17 |
+
name: str
|
| 18 |
+
arguments: dict[str, Any] # Function arguments as dict
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ToolCall(BaseModel):
|
| 22 |
+
"""Tool call structure."""
|
| 23 |
+
|
| 24 |
+
id: str
|
| 25 |
+
type: str # "function"
|
| 26 |
+
function: FunctionCall
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class Message(BaseModel):
|
| 30 |
+
"""Chat message."""
|
| 31 |
+
|
| 32 |
+
role: str # "system", "user", "assistant", "tool"
|
| 33 |
+
content: str | list[dict[str, Any]] # Can be string or list of content blocks
|
| 34 |
+
thinking: str | None = None # Extended thinking content for assistant messages
|
| 35 |
+
tool_calls: list[ToolCall] | None = None
|
| 36 |
+
tool_call_id: str | None = None
|
| 37 |
+
name: str | None = None # For tool role
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class TokenUsage(BaseModel):
|
| 41 |
+
"""Token usage statistics from LLM API response."""
|
| 42 |
+
|
| 43 |
+
prompt_tokens: int = 0
|
| 44 |
+
completion_tokens: int = 0
|
| 45 |
+
total_tokens: int = 0
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class LLMResponse(BaseModel):
|
| 49 |
+
"""LLM response."""
|
| 50 |
+
|
| 51 |
+
content: str
|
| 52 |
+
thinking: str | None = None # Extended thinking blocks
|
| 53 |
+
tool_calls: list[ToolCall] | None = None
|
| 54 |
+
finish_reason: str
|
| 55 |
+
usage: TokenUsage | None = None # Token usage from API response
|
mini_agent/skills/.claude-plugin/marketplace.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "anthropic-agent-skills",
|
| 3 |
+
"owner": {
|
| 4 |
+
"name": "Keith Lazuka",
|
| 5 |
+
"email": "klazuka@anthropic.com"
|
| 6 |
+
},
|
| 7 |
+
"metadata": {
|
| 8 |
+
"description": "Anthropic example skills",
|
| 9 |
+
"version": "1.0.0"
|
| 10 |
+
},
|
| 11 |
+
"plugins": [
|
| 12 |
+
{
|
| 13 |
+
"name": "document-skills",
|
| 14 |
+
"description": "Collection of document processing suite including Excel, Word, PowerPoint, and PDF capabilities",
|
| 15 |
+
"source": "./",
|
| 16 |
+
"strict": false,
|
| 17 |
+
"skills": [
|
| 18 |
+
"./document-skills/xlsx",
|
| 19 |
+
"./document-skills/docx",
|
| 20 |
+
"./document-skills/pptx",
|
| 21 |
+
"./document-skills/pdf"
|
| 22 |
+
]
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"name": "example-skills",
|
| 26 |
+
"description": "Collection of example skills demonstrating various capabilities including skill creation, MCP building, visual design, algorithmic art, internal communications, web testing, artifact building, Slack GIFs, and theme styling",
|
| 27 |
+
"source": "./",
|
| 28 |
+
"strict": false,
|
| 29 |
+
"skills": [
|
| 30 |
+
"./skill-creator",
|
| 31 |
+
"./mcp-builder",
|
| 32 |
+
"./canvas-design",
|
| 33 |
+
"./algorithmic-art",
|
| 34 |
+
"./internal-comms",
|
| 35 |
+
"./webapp-testing",
|
| 36 |
+
"./artifacts-builder",
|
| 37 |
+
"./slack-gif-creator",
|
| 38 |
+
"./theme-factory",
|
| 39 |
+
"./brand-guidelines"
|
| 40 |
+
]
|
| 41 |
+
}
|
| 42 |
+
]
|
| 43 |
+
}
|
mini_agent/skills/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
|
mini_agent/skills/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Skills
|
| 2 |
+
Skills are folders of instructions, scripts, and resources that Claude loads dynamically to improve performance on specialized tasks. Skills teach Claude how to complete specific tasks in a repeatable way, whether that's creating documents with your company's brand guidelines, analyzing data using your organization's specific workflows, or automating personal tasks.
|
| 3 |
+
|
| 4 |
+
For more information, check out:
|
| 5 |
+
- [What are skills?](https://support.claude.com/en/articles/12512176-what-are-skills)
|
| 6 |
+
- [Using skills in Claude](https://support.claude.com/en/articles/12512180-using-skills-in-claude)
|
| 7 |
+
- [How to create custom skills](https://support.claude.com/en/articles/12512198-creating-custom-skills)
|
| 8 |
+
- [Equipping agents for the real world with Agent Skills](https://anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)
|
| 9 |
+
|
| 10 |
+
# About This Repository
|
| 11 |
+
|
| 12 |
+
This repository contains example skills that demonstrate what's possible with Claude's skills system. These examples range from creative applications (art, music, design) to technical tasks (testing web apps, MCP server generation) to enterprise workflows (communications, branding, etc.).
|
| 13 |
+
|
| 14 |
+
Each skill is self-contained in its own directory with a `SKILL.md` file containing the instructions and metadata that Claude uses. Browse through these examples to get inspiration for your own skills or to understand different patterns and approaches.
|
| 15 |
+
|
| 16 |
+
The example skills in this repo are open source (Apache 2.0). We've also included the document creation & editing skills that power [Claude's document capabilities](https://www.anthropic.com/news/create-files) under the hood in the [`document-skills/`](./document-skills/) folder. These are source-available, not open source, but we wanted to share these with developers as a reference for more complex skills that are actively used in a production AI application.
|
| 17 |
+
|
| 18 |
+
**Note:** These are reference examples for inspiration and learning. They showcase general-purpose capabilities rather than organization-specific workflows or sensitive content.
|
| 19 |
+
|
| 20 |
+
## Disclaimer
|
| 21 |
+
|
| 22 |
+
**These skills are provided for demonstration and educational purposes only.** While some of these capabilities may be available in Claude, the implementations and behaviors you receive from Claude may differ from what is shown in these examples. These examples are meant to illustrate patterns and possibilities. Always test skills thoroughly in your own environment before relying on them for critical tasks.
|
| 23 |
+
|
| 24 |
+
# Example Skills
|
| 25 |
+
|
| 26 |
+
This repository includes a diverse collection of example skills demonstrating different capabilities:
|
| 27 |
+
|
| 28 |
+
## Creative & Design
|
| 29 |
+
- **algorithmic-art** - Create generative art using p5.js with seeded randomness, flow fields, and particle systems
|
| 30 |
+
- **canvas-design** - Design beautiful visual art in .png and .pdf formats using design philosophies
|
| 31 |
+
- **slack-gif-creator** - Create animated GIFs optimized for Slack's size constraints
|
| 32 |
+
|
| 33 |
+
## Development & Technical
|
| 34 |
+
- **artifacts-builder** - Build complex claude.ai HTML artifacts using React, Tailwind CSS, and shadcn/ui components
|
| 35 |
+
- **mcp-server** - Guide for creating high-quality MCP servers to integrate external APIs and services
|
| 36 |
+
- **webapp-testing** - Test local web applications using Playwright for UI verification and debugging
|
| 37 |
+
|
| 38 |
+
## Enterprise & Communication
|
| 39 |
+
- **brand-guidelines** - Apply Anthropic's official brand colors and typography to artifacts
|
| 40 |
+
- **internal-comms** - Write internal communications like status reports, newsletters, and FAQs
|
| 41 |
+
- **theme-factory** - Style artifacts with 10 pre-set professional themes or generate custom themes on-the-fly
|
| 42 |
+
|
| 43 |
+
## Meta Skills
|
| 44 |
+
- **skill-creator** - Guide for creating effective skills that extend Claude's capabilities
|
| 45 |
+
- **template-skill** - A basic template to use as a starting point for new skills
|
| 46 |
+
|
| 47 |
+
# Document Skills
|
| 48 |
+
|
| 49 |
+
The `document-skills/` subdirectory contains skills that Anthropic developed to help Claude create various document file formats. These skills demonstrate advanced patterns for working with complex file formats and binary data:
|
| 50 |
+
|
| 51 |
+
- **docx** - Create, edit, and analyze Word documents with support for tracked changes, comments, formatting preservation, and text extraction
|
| 52 |
+
- **pdf** - Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms
|
| 53 |
+
- **pptx** - Create, edit, and analyze PowerPoint presentations with support for layouts, templates, charts, and automated slide generation
|
| 54 |
+
- **xlsx** - Create, edit, and analyze Excel spreadsheets with support for formulas, formatting, data analysis, and visualization
|
| 55 |
+
|
| 56 |
+
**Important Disclaimer:** These document skills are point-in-time snapshots and are not actively maintained or updated. Versions of these skills ship pre-included with Claude. They are primarily intended as reference examples to illustrate how Anthropic approaches developing more complex skills that work with binary file formats and document structures.
|
| 57 |
+
|
| 58 |
+
# Try in Claude Code, Claude.ai, and the API
|
| 59 |
+
|
| 60 |
+
## Claude Code
|
| 61 |
+
You can register this repository as a Claude Code Plugin marketplace by running the following command in Claude Code:
|
| 62 |
+
```
|
| 63 |
+
/plugin marketplace add anthropics/skills
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
Then, to install a specific set of skills:
|
| 67 |
+
1. Select `Browse and install plugins`
|
| 68 |
+
2. Select `anthropic-agent-skills`
|
| 69 |
+
3. Select `document-skills` or `example-skills`
|
| 70 |
+
4. Select `Install now`
|
| 71 |
+
|
| 72 |
+
Alternatively, directly install either Plugin via:
|
| 73 |
+
```
|
| 74 |
+
/plugin install document-skills@anthropic-agent-skills
|
| 75 |
+
/plugin install example-skills@anthropic-agent-skills
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
After installing the plugin, you can use the skill by just mentioning it. For instance, if you install the `document-skills` plugin from the marketplace, you can ask Claude Code to do something like: "Use the PDF skill to extract the form fields from path/to/some-file.pdf"
|
| 79 |
+
|
| 80 |
+
## Claude.ai
|
| 81 |
+
|
| 82 |
+
These example skills are all already available to paid plans in Claude.ai.
|
| 83 |
+
|
| 84 |
+
To use any skill from this repository or upload custom skills, follow the instructions in [Using skills in Claude](https://support.claude.com/en/articles/12512180-using-skills-in-claude#h_a4222fa77b).
|
| 85 |
+
|
| 86 |
+
## Claude API
|
| 87 |
+
|
| 88 |
+
You can use Anthropic's pre-built skills, and upload custom skills, via the Claude API. See the [Skills API Quickstart](https://docs.claude.com/en/api/skills-guide#creating-a-skill) for more.
|
| 89 |
+
|
| 90 |
+
# Creating a Basic Skill
|
| 91 |
+
|
| 92 |
+
Skills are simple to create - just a folder with a `SKILL.md` file containing YAML frontmatter and instructions. You can use the **template-skill** in this repository as a starting point:
|
| 93 |
+
|
| 94 |
+
```markdown
|
| 95 |
+
---
|
| 96 |
+
name: my-skill-name
|
| 97 |
+
description: A clear description of what this skill does and when to use it
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
# My Skill Name
|
| 101 |
+
|
| 102 |
+
[Add your instructions here that Claude will follow when this skill is active]
|
| 103 |
+
|
| 104 |
+
## Examples
|
| 105 |
+
- Example usage 1
|
| 106 |
+
- Example usage 2
|
| 107 |
+
|
| 108 |
+
## Guidelines
|
| 109 |
+
- Guideline 1
|
| 110 |
+
- Guideline 2
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
The frontmatter requires only two fields:
|
| 114 |
+
- `name` - A unique identifier for your skill (lowercase, hyphens for spaces)
|
| 115 |
+
- `description` - A complete description of what the skill does and when to use it
|
| 116 |
+
|
| 117 |
+
The markdown content below contains the instructions, examples, and guidelines that Claude will follow. For more details, see [How to create custom skills](https://support.claude.com/en/articles/12512198-creating-custom-skills).
|
| 118 |
+
|
| 119 |
+
# Partner Skills
|
| 120 |
+
|
| 121 |
+
Skills are a great way to teach Claude how to get better at using specific pieces of software. As we see awesome example skills from partners, we may highlight some of them here:
|
| 122 |
+
|
| 123 |
+
- **Notion** - [Notion Skills for Claude](https://www.notion.so/notiondevs/Notion-Skills-for-Claude-28da4445d27180c7af1df7d8615723d0)
|
mini_agent/skills/THIRD_PARTY_NOTICES.md
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **Third-Party Notices**
|
| 2 |
+
|
| 3 |
+
THE FOLLOWING SETS FORTH ATTRIBUTION NOTICES FOR THIRD PARTY SOFTWARE THAT MAY BE CONTAINED IN PORTIONS OF THIS PRODUCT.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## **BSD 2-Clause License**
|
| 8 |
+
|
| 9 |
+
The following components are licensed under BSD 2-Clause License reproduced below:
|
| 10 |
+
|
| 11 |
+
**imageio 2.37.0**, Copyright (c) 2014-2022, imageio developers
|
| 12 |
+
|
| 13 |
+
**imageio-ffmpeg 0.6.0**, Copyright (c) 2019-2025, imageio
|
| 14 |
+
|
| 15 |
+
**License Text:**
|
| 16 |
+
|
| 17 |
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
| 18 |
+
|
| 19 |
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
| 20 |
+
|
| 21 |
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
| 22 |
+
|
| 23 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## **GNU General Public License v3.0**
|
| 28 |
+
|
| 29 |
+
The following components are licensed under GNU General Public License v3.0 reproduced below:
|
| 30 |
+
|
| 31 |
+
**FFmpeg 7.0.2**, Copyright (c) 2000-2024 the FFmpeg developers
|
| 32 |
+
|
| 33 |
+
Source Code: [https://ffmpeg.org/releases/ffmpeg-7.0.2.tar.xz](https://ffmpeg.org/releases/ffmpeg-7.0.2.tar.xz)
|
| 34 |
+
|
| 35 |
+
**License Text:**
|
| 36 |
+
|
| 37 |
+
GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
|
| 38 |
+
|
| 39 |
+
Copyright © 2007 Free Software Foundation, Inc. [https://fsf.org/](https://fsf.org/)
|
| 40 |
+
|
| 41 |
+
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
| 42 |
+
|
| 43 |
+
Preamble
|
| 44 |
+
|
| 45 |
+
The GNU General Public License is a free, copyleft license for software and other kinds of works.
|
| 46 |
+
|
| 47 |
+
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
|
| 48 |
+
|
| 49 |
+
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
|
| 50 |
+
|
| 51 |
+
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
|
| 52 |
+
|
| 53 |
+
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
| 54 |
+
|
| 55 |
+
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
|
| 56 |
+
|
| 57 |
+
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
|
| 58 |
+
|
| 59 |
+
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
|
| 60 |
+
|
| 61 |
+
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
|
| 62 |
+
|
| 63 |
+
The precise terms and conditions for copying, distribution and modification follow.
|
| 64 |
+
|
| 65 |
+
TERMS AND CONDITIONS
|
| 66 |
+
|
| 67 |
+
0. Definitions.
|
| 68 |
+
|
| 69 |
+
"This License" refers to version 3 of the GNU General Public License.
|
| 70 |
+
|
| 71 |
+
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
|
| 72 |
+
|
| 73 |
+
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
|
| 74 |
+
|
| 75 |
+
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
|
| 76 |
+
|
| 77 |
+
A "covered work" means either the unmodified Program or a work based on the Program.
|
| 78 |
+
|
| 79 |
+
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
|
| 80 |
+
|
| 81 |
+
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
|
| 82 |
+
|
| 83 |
+
An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
| 84 |
+
|
| 85 |
+
1. Source Code.
|
| 86 |
+
|
| 87 |
+
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
|
| 88 |
+
|
| 89 |
+
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
|
| 90 |
+
|
| 91 |
+
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
|
| 92 |
+
|
| 93 |
+
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
|
| 94 |
+
|
| 95 |
+
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
|
| 96 |
+
|
| 97 |
+
The Corresponding Source for a work in source code form is that same work.
|
| 98 |
+
|
| 99 |
+
2. Basic Permissions.
|
| 100 |
+
|
| 101 |
+
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
| 102 |
+
|
| 103 |
+
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
| 104 |
+
|
| 105 |
+
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
|
| 106 |
+
|
| 107 |
+
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
| 108 |
+
|
| 109 |
+
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
|
| 110 |
+
|
| 111 |
+
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
|
| 112 |
+
|
| 113 |
+
4. Conveying Verbatim Copies.
|
| 114 |
+
|
| 115 |
+
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
| 116 |
+
|
| 117 |
+
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
|
| 118 |
+
|
| 119 |
+
5. Conveying Modified Source Versions.
|
| 120 |
+
|
| 121 |
+
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
|
| 122 |
+
|
| 123 |
+
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
|
| 124 |
+
|
| 125 |
+
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7\. This requirement modifies the requirement in section 4 to "keep intact all notices".
|
| 126 |
+
|
| 127 |
+
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
|
| 128 |
+
|
| 129 |
+
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
|
| 130 |
+
|
| 131 |
+
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
|
| 132 |
+
|
| 133 |
+
6. Conveying Non-Source Forms.
|
| 134 |
+
|
| 135 |
+
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
| 136 |
+
|
| 137 |
+
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
|
| 138 |
+
|
| 139 |
+
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
|
| 140 |
+
|
| 141 |
+
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
|
| 142 |
+
|
| 143 |
+
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
|
| 144 |
+
|
| 145 |
+
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
|
| 146 |
+
|
| 147 |
+
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
|
| 148 |
+
|
| 149 |
+
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
|
| 150 |
+
|
| 151 |
+
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
|
| 152 |
+
|
| 153 |
+
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
|
| 154 |
+
|
| 155 |
+
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
|
| 156 |
+
|
| 157 |
+
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
|
| 158 |
+
|
| 159 |
+
7. Additional Terms.
|
| 160 |
+
|
| 161 |
+
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
|
| 162 |
+
|
| 163 |
+
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
|
| 164 |
+
|
| 165 |
+
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
|
| 166 |
+
|
| 167 |
+
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
|
| 168 |
+
|
| 169 |
+
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
|
| 170 |
+
|
| 171 |
+
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
|
| 172 |
+
|
| 173 |
+
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
|
| 174 |
+
|
| 175 |
+
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
|
| 176 |
+
|
| 177 |
+
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
|
| 178 |
+
|
| 179 |
+
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10\. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
|
| 180 |
+
|
| 181 |
+
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
|
| 182 |
+
|
| 183 |
+
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
|
| 184 |
+
|
| 185 |
+
8. Termination.
|
| 186 |
+
|
| 187 |
+
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
|
| 188 |
+
|
| 189 |
+
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
|
| 190 |
+
|
| 191 |
+
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
|
| 192 |
+
|
| 193 |
+
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10\.
|
| 194 |
+
|
| 195 |
+
9. Acceptance Not Required for Having Copies.
|
| 196 |
+
|
| 197 |
+
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
|
| 198 |
+
|
| 199 |
+
10. Automatic Licensing of Downstream Recipients.
|
| 200 |
+
|
| 201 |
+
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
|
| 202 |
+
|
| 203 |
+
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
|
| 204 |
+
|
| 205 |
+
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
|
| 206 |
+
|
| 207 |
+
11. Patents.
|
| 208 |
+
|
| 209 |
+
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
|
| 210 |
+
|
| 211 |
+
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
|
| 212 |
+
|
| 213 |
+
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
|
| 214 |
+
|
| 215 |
+
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
|
| 216 |
+
|
| 217 |
+
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
|
| 218 |
+
|
| 219 |
+
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
|
| 220 |
+
|
| 221 |
+
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007\.
|
| 222 |
+
|
| 223 |
+
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
|
| 224 |
+
|
| 225 |
+
12. No Surrender of Others' Freedom.
|
| 226 |
+
|
| 227 |
+
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
|
| 228 |
+
|
| 229 |
+
13. Use with the GNU Affero General Public License.
|
| 230 |
+
|
| 231 |
+
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
|
| 232 |
+
|
| 233 |
+
14. Revised Versions of this License.
|
| 234 |
+
|
| 235 |
+
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
| 236 |
+
|
| 237 |
+
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
|
| 238 |
+
|
| 239 |
+
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
|
| 240 |
+
|
| 241 |
+
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
|
| 242 |
+
|
| 243 |
+
15. Disclaimer of Warranty.
|
| 244 |
+
|
| 245 |
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
| 246 |
+
|
| 247 |
+
16. Limitation of Liability.
|
| 248 |
+
|
| 249 |
+
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
| 250 |
+
|
| 251 |
+
17. Interpretation of Sections 15 and 16\.
|
| 252 |
+
|
| 253 |
+
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
|
| 254 |
+
|
| 255 |
+
END OF TERMS AND CONDITIONS
|
| 256 |
+
|
| 257 |
+
How to Apply These Terms to Your New Programs
|
| 258 |
+
|
| 259 |
+
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
| 260 |
+
|
| 261 |
+
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
| 262 |
+
|
| 263 |
+
\<one line to give the program's name and a brief idea of what it does.\>
|
| 264 |
+
Copyright (C) \<year\> \<name of author\>
|
| 265 |
+
|
| 266 |
+
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
| 267 |
+
|
| 268 |
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
| 269 |
+
|
| 270 |
+
You should have received a copy of the GNU General Public License along with this program. If not, see [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/).
|
| 271 |
+
|
| 272 |
+
Also add information on how to contact you by electronic and paper mail.
|
| 273 |
+
|
| 274 |
+
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
| 275 |
+
|
| 276 |
+
\<program\> Copyright (C) \<year\> \<name of author\>
|
| 277 |
+
This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free software, and you are welcome to redistribute it under certain conditions; type 'show c' for details.
|
| 278 |
+
|
| 279 |
+
The hypothetical commands 'show w' and 'show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box".
|
| 280 |
+
|
| 281 |
+
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/).
|
| 282 |
+
|
| 283 |
+
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read [https://www.gnu.org/licenses/why-not-lgpl.html](https://www.gnu.org/licenses/why-not-lgpl.html).
|
| 284 |
+
|
| 285 |
+
---
|
| 286 |
+
|
| 287 |
+
## **MIT-CMU License (HPND)**
|
| 288 |
+
|
| 289 |
+
The following components are licensed under MIT-CMU License (HPND) reproduced below:
|
| 290 |
+
|
| 291 |
+
**Pillow 11.3.0**, Copyright © 1997-2011 by Secret Labs AB, Copyright © 1995-2011 by Fredrik Lundh and contributors, Copyright © 2010 by Jeffrey A. Clark and contributors
|
| 292 |
+
|
| 293 |
+
**License Text:**
|
| 294 |
+
|
| 295 |
+
By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions:
|
| 296 |
+
|
| 297 |
+
Permission to use, copy, modify and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission.
|
| 298 |
+
|
| 299 |
+
SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
| 300 |
+
|
| 301 |
+
---
|
| 302 |
+
|
| 303 |
+
## **SIL Open Font License v1.1**
|
| 304 |
+
|
| 305 |
+
The following fonts are licensed under SIL Open Font License v1.1 reproduced below:
|
| 306 |
+
|
| 307 |
+
**Arsenal SC**, Copyright 2012 The Arsenal Project Authors ([andrij.design@gmail.com](mailto:andrij.design@gmail.com))
|
| 308 |
+
|
| 309 |
+
**Big Shoulders**, Copyright 2019 The Big Shoulders Project Authors ([https://github.com/xotypeco/big\_shoulders](https://github.com/xotypeco/big_shoulders))
|
| 310 |
+
|
| 311 |
+
**Boldonse**, Copyright 2024 The Boldonse Project Authors ([https://github.com/googlefonts/boldonse](https://github.com/googlefonts/boldonse))
|
| 312 |
+
|
| 313 |
+
**Bricolage Grotesque**, Copyright 2022 The Bricolage Grotesque Project Authors ([https://github.com/ateliertriay/bricolage](https://github.com/ateliertriay/bricolage))
|
| 314 |
+
|
| 315 |
+
**Crimson Pro**, Copyright 2018 The Crimson Pro Project Authors ([https://github.com/Fonthausen/CrimsonPro](https://github.com/Fonthausen/CrimsonPro))
|
| 316 |
+
|
| 317 |
+
**DM Mono**, Copyright 2020 The DM Mono Project Authors ([https://www.github.com/googlefonts/dm-mono](https://www.github.com/googlefonts/dm-mono))
|
| 318 |
+
|
| 319 |
+
**Erica One**, Copyright (c) 2011 by LatinoType Limitada ([luciano@latinotype.com](mailto:luciano@latinotype.com)), with Reserved Font Name "Erica One"
|
| 320 |
+
|
| 321 |
+
**Geist Mono**, Copyright 2024 The Geist Project Authors ([https://github.com/vercel/geist-font.git](https://github.com/vercel/geist-font.git))
|
| 322 |
+
|
| 323 |
+
**Gloock**, Copyright 2022 The Gloock Project Authors ([https://github.com/duartp/gloock](https://github.com/duartp/gloock))
|
| 324 |
+
|
| 325 |
+
**IBM Plex Mono**, Copyright © 2017 IBM Corp., with Reserved Font Name "Plex"
|
| 326 |
+
|
| 327 |
+
**Instrument Sans**, Copyright 2022 The Instrument Sans Project Authors ([https://github.com/Instrument/instrument-sans](https://github.com/Instrument/instrument-sans))
|
| 328 |
+
|
| 329 |
+
**Italiana**, Copyright (c) 2011, Santiago Orozco ([hi@typemade.mx](mailto:hi@typemade.mx)), with Reserved Font Name "Italiana"
|
| 330 |
+
|
| 331 |
+
**JetBrains Mono**, Copyright 2020 The JetBrains Mono Project Authors ([https://github.com/JetBrains/JetBrainsMono](https://github.com/JetBrains/JetBrainsMono))
|
| 332 |
+
|
| 333 |
+
**Jura**, Copyright 2019 The Jura Project Authors ([https://github.com/ossobuffo/jura](https://github.com/ossobuffo/jura))
|
| 334 |
+
|
| 335 |
+
**Libre Baskerville**, Copyright 2012 The Libre Baskerville Project Authors ([https://github.com/impallari/Libre-Baskerville](https://github.com/impallari/Libre-Baskerville)), with Reserved Font Name "Libre Baskerville"
|
| 336 |
+
|
| 337 |
+
**Lora**, Copyright 2011 The Lora Project Authors ([https://github.com/cyrealtype/Lora-Cyrillic](https://github.com/cyrealtype/Lora-Cyrillic)), with Reserved Font Name "Lora"
|
| 338 |
+
|
| 339 |
+
**National Park**, Copyright 2025 The National Park Project Authors ([https://github.com/benhoepner/National-Park](https://github.com/benhoepner/National-Park))
|
| 340 |
+
|
| 341 |
+
**Nothing You Could Do**, Copyright (c) 2010, Kimberly Geswein (kimberlygeswein.com)
|
| 342 |
+
|
| 343 |
+
**Outfit**, Copyright 2021 The Outfit Project Authors ([https://github.com/Outfitio/Outfit-Fonts](https://github.com/Outfitio/Outfit-Fonts))
|
| 344 |
+
|
| 345 |
+
**Pixelify Sans**, Copyright 2021 The Pixelify Sans Project Authors ([https://github.com/eifetx/Pixelify-Sans](https://github.com/eifetx/Pixelify-Sans))
|
| 346 |
+
|
| 347 |
+
**Poiret One**, Copyright (c) 2011, Denis Masharov ([denis.masharov@gmail.com](mailto:denis.masharov@gmail.com))
|
| 348 |
+
|
| 349 |
+
**Red Hat Mono**, Copyright 2024 The Red Hat Project Authors ([https://github.com/RedHatOfficial/RedHatFont](https://github.com/RedHatOfficial/RedHatFont))
|
| 350 |
+
|
| 351 |
+
**Silkscreen**, Copyright 2001 The Silkscreen Project Authors ([https://github.com/googlefonts/silkscreen](https://github.com/googlefonts/silkscreen))
|
| 352 |
+
|
| 353 |
+
**Smooch Sans**, Copyright 2016 The Smooch Sans Project Authors ([https://github.com/googlefonts/smooch-sans](https://github.com/googlefonts/smooch-sans))
|
| 354 |
+
|
| 355 |
+
**Tektur**, Copyright 2023 The Tektur Project Authors ([https://www.github.com/hyvyys/Tektur](https://www.github.com/hyvyys/Tektur))
|
| 356 |
+
|
| 357 |
+
**Work Sans**, Copyright 2019 The Work Sans Project Authors ([https://github.com/weiweihuanghuang/Work-Sans](https://github.com/weiweihuanghuang/Work-Sans))
|
| 358 |
+
|
| 359 |
+
**Young Serif**, Copyright 2023 The Young Serif Project Authors ([https://github.com/noirblancrouge/YoungSerif](https://github.com/noirblancrouge/YoungSerif))
|
| 360 |
+
|
| 361 |
+
**License Text:**
|
| 362 |
+
|
| 363 |
+
---
|
| 364 |
+
|
| 365 |
+
## **SIL OPEN FONT LICENSE Version 1.1 \- 26 February 2007**
|
| 366 |
+
|
| 367 |
+
PREAMBLE
|
| 368 |
+
|
| 369 |
+
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
|
| 370 |
+
|
| 371 |
+
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
|
| 372 |
+
|
| 373 |
+
DEFINITIONS
|
| 374 |
+
|
| 375 |
+
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
|
| 376 |
+
|
| 377 |
+
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
|
| 378 |
+
|
| 379 |
+
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
|
| 380 |
+
|
| 381 |
+
"Modified Version" refers to any derivative made by adding to, deleting, or substituting \-- in part or in whole \-- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
|
| 382 |
+
|
| 383 |
+
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
|
| 384 |
+
|
| 385 |
+
PERMISSION & CONDITIONS
|
| 386 |
+
|
| 387 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
|
| 388 |
+
|
| 389 |
+
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
|
| 390 |
+
|
| 391 |
+
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
|
| 392 |
+
|
| 393 |
+
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
|
| 394 |
+
|
| 395 |
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
|
| 396 |
+
|
| 397 |
+
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
|
| 398 |
+
|
| 399 |
+
TERMINATION
|
| 400 |
+
|
| 401 |
+
This license becomes null and void if any of the above conditions are not met.
|
| 402 |
+
|
| 403 |
+
DISCLAIMER
|
| 404 |
+
|
| 405 |
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
mini_agent/skills/agent_skills_spec.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent Skills Spec
|
| 2 |
+
|
| 3 |
+
A skill is a folder of instructions, scripts, and resources that agents can discover and load dynamically to perform better at specific tasks. In order for the folder to be recognized as a skill, it must contain a `SKILL.md` file.
|
| 4 |
+
|
| 5 |
+
# Skill Folder Layout
|
| 6 |
+
|
| 7 |
+
A minimal skill folder looks like this:
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
my-skill/
|
| 11 |
+
- SKILL.md
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
More complex skills can add additional directories and files as needed.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# The SKILL.md file
|
| 18 |
+
|
| 19 |
+
The skill's "entrypoint" is the `SKILL.md` file. It is the only file required to exist. The file must start with a YAML frontmatter followed by regular Markdown.
|
| 20 |
+
|
| 21 |
+
## YAML Frontmatter
|
| 22 |
+
|
| 23 |
+
The YAML frontmatter has 2 required properties:
|
| 24 |
+
|
| 25 |
+
- `name`
|
| 26 |
+
- The name of the skill in hyphen-case
|
| 27 |
+
- Restricted to lowercase Unicode alphanumeric + hyphen
|
| 28 |
+
- Must match the name of the directory containing the SKILL.md
|
| 29 |
+
- `description`
|
| 30 |
+
- Description of what the skill does and when Claude should use it
|
| 31 |
+
|
| 32 |
+
There are 3 optional properties:
|
| 33 |
+
|
| 34 |
+
- `license`
|
| 35 |
+
- The license applied to the skill
|
| 36 |
+
- We recommend keeping it short (either the name of a license or the name of a bundled license file)
|
| 37 |
+
- `allowed-tools`
|
| 38 |
+
- A list of tools that are pre-approved to run
|
| 39 |
+
- Currently only supported in Claude Code
|
| 40 |
+
- `metadata`
|
| 41 |
+
- A map from string keys to string values
|
| 42 |
+
- Clients can use this to store additional properties not defined by the Agent Skills Spec
|
| 43 |
+
- We recommend making your key names reasonably unique to avoid accidental conflicts
|
| 44 |
+
|
| 45 |
+
## Markdown Body
|
| 46 |
+
|
| 47 |
+
The Markdown body has no restrictions on it.
|
| 48 |
+
|
| 49 |
+
# Additional Information
|
| 50 |
+
|
| 51 |
+
For a minimal example, see the `template-skill` example.
|
| 52 |
+
|
| 53 |
+
# Version History
|
| 54 |
+
|
| 55 |
+
- 1.0 (2025-10-16) Public Launch
|
mini_agent/skills/algorithmic-art/LICENSE.txt
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|