echo-agent 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {echo_agent-0.2.0 → echo_agent-0.2.1}/PKG-INFO +20 -7
- {echo_agent-0.2.0 → echo_agent-0.2.1}/README.md +1 -6
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/compressor.py +8 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/context.py +149 -10
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/pipeline/context_stage.py +48 -11
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/__init__.py +89 -11
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/tts.py +9 -5
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/base.py +42 -1
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/feishu.py +23 -1
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/matrix.py +29 -2
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/slack.py +24 -4
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/telegram.py +28 -2
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/weixin.py +21 -6
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/whatsapp.py +29 -3
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/config/schema.py +4 -0
- echo_agent-0.2.1/echo_agent/dependencies/__init__.py +37 -0
- echo_agent-0.2.1/echo_agent/dependencies/cli.py +167 -0
- echo_agent-0.2.1/echo_agent/dependencies/lazy_deps.py +488 -0
- echo_agent-0.2.1/echo_agent/dependencies/skill_require.py +133 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/consolidator.py +14 -1
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/bedrock_provider.py +28 -2
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/format_utils.py +33 -3
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/gemini_provider.py +24 -2
- echo_agent-0.2.1/echo_agent/session/media_ref.py +42 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/pyproject.toml +20 -1
- echo_agent-0.2.1/skills/creative/excel-author/SKILL.md +88 -0
- echo_agent-0.2.1/skills/creative/excel-author/scripts/create_xlsx.py +97 -0
- echo_agent-0.2.1/skills/creative/image-gen/SKILL.md +74 -0
- echo_agent-0.2.1/skills/creative/image-gen/scripts/generate_image.py +73 -0
- echo_agent-0.2.1/skills/creative/meme-gen/SKILL.md +66 -0
- echo_agent-0.2.1/skills/creative/meme-gen/scripts/make_meme.py +121 -0
- echo_agent-0.2.1/skills/creative/ppt-author/SKILL.md +87 -0
- echo_agent-0.2.1/skills/creative/ppt-author/scripts/create_pptx.py +89 -0
- echo_agent-0.2.1/skills/development/code-runner/SKILL.md +71 -0
- echo_agent-0.2.1/skills/development/code-runner/scripts/safe_exec.py +133 -0
- echo_agent-0.2.1/skills/development/github-ops/SKILL.md +81 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/development/skill-creator/scripts/init_skill.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/development/skill-creator/scripts/package_skill.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/development/skill-creator/scripts/quick_validate.py +0 -0
- echo_agent-0.2.1/skills/development/workflow-chain/SKILL.md +103 -0
- echo_agent-0.2.1/skills/development/workflow-chain/scripts/workflow_engine.py +100 -0
- echo_agent-0.2.1/skills/devops/docker-manage/SKILL.md +85 -0
- echo_agent-0.2.1/skills/devops/system-monitor/SKILL.md +77 -0
- echo_agent-0.2.1/skills/devops/system-monitor/scripts/system_check.py +110 -0
- echo_agent-0.2.1/skills/finance/finance-tracker/SKILL.md +66 -0
- echo_agent-0.2.1/skills/finance/finance-tracker/scripts/finance_manager.py +141 -0
- echo_agent-0.2.1/skills/finance/stocks/SKILL.md +75 -0
- echo_agent-0.2.1/skills/finance/stocks/scripts/market_query.py +99 -0
- echo_agent-0.2.1/skills/health/fitness-nutrition/SKILL.md +68 -0
- echo_agent-0.2.1/skills/health/fitness-nutrition/scripts/health_query.py +83 -0
- echo_agent-0.2.1/skills/learning/flashcards/SKILL.md +80 -0
- echo_agent-0.2.1/skills/learning/flashcards/scripts/flashcard_engine.py +170 -0
- echo_agent-0.2.1/skills/media/tts-voice/SKILL.md +82 -0
- echo_agent-0.2.1/skills/media/tts-voice/scripts/text_to_speech.py +55 -0
- echo_agent-0.2.1/skills/media/voice-note/SKILL.md +87 -0
- echo_agent-0.2.1/skills/media/voice-note/scripts/voice_process.py +68 -0
- echo_agent-0.2.1/skills/productivity/calendar/SKILL.md +64 -0
- echo_agent-0.2.1/skills/productivity/calendar/scripts/calendar_client.py +96 -0
- echo_agent-0.2.1/skills/productivity/daily-briefing/SKILL.md +69 -0
- echo_agent-0.2.1/skills/productivity/daily-briefing/scripts/generate_briefing.py +57 -0
- echo_agent-0.2.1/skills/productivity/email-assistant/SKILL.md +80 -0
- echo_agent-0.2.1/skills/productivity/email-assistant/scripts/email_client.py +99 -0
- echo_agent-0.2.1/skills/productivity/note-taking/SKILL.md +60 -0
- echo_agent-0.2.1/skills/productivity/note-taking/scripts/notes_manager.py +126 -0
- echo_agent-0.2.1/skills/productivity/notion-sync/SKILL.md +80 -0
- echo_agent-0.2.1/skills/productivity/notion-sync/scripts/notion_client.py +81 -0
- echo_agent-0.2.1/skills/productivity/ocr-document/SKILL.md +86 -0
- echo_agent-0.2.1/skills/productivity/ocr-document/scripts/extract_document.py +75 -0
- echo_agent-0.2.1/skills/productivity/reminder/SKILL.md +65 -0
- echo_agent-0.2.1/skills/productivity/reminder/scripts/reminder_store.py +112 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/research/arxiv/scripts/search_arxiv.py +0 -0
- echo_agent-0.2.1/skills/research/deep-research/SKILL.md +74 -0
- echo_agent-0.2.1/skills/research/deep-research/scripts/research_report.py +95 -0
- echo_agent-0.2.1/skills/research/rss-watcher/SKILL.md +70 -0
- echo_agent-0.2.1/skills/research/rss-watcher/scripts/feed_monitor.py +100 -0
- echo_agent-0.2.1/skills/research/web-extract/SKILL.md +82 -0
- echo_agent-0.2.1/skills/research/web-extract/scripts/extract_url.py +38 -0
- echo_agent-0.2.1/skills/research/web-search/SKILL.md +80 -0
- echo_agent-0.2.1/skills/research/web-search/scripts/web_search.py +54 -0
- echo_agent-0.2.1/skills/utility/calculator/SKILL.md +91 -0
- echo_agent-0.2.1/skills/utility/calculator/scripts/calc.py +103 -0
- echo_agent-0.2.1/skills/utility/file-convert/SKILL.md +59 -0
- echo_agent-0.2.1/skills/utility/file-convert/scripts/convert.py +83 -0
- echo_agent-0.2.1/skills/utility/maps-poi/SKILL.md +77 -0
- echo_agent-0.2.1/skills/utility/maps-poi/scripts/geo_query.py +86 -0
- echo_agent-0.2.1/skills/utility/text-tools/SKILL.md +86 -0
- echo_agent-0.2.1/skills/utility/text-tools/scripts/text_process.py +94 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/.gitignore +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/LICENSE +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/__main__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/a2a/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/a2a/client.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/a2a/models.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/a2a/protocol.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/a2a/server.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/approval_gate.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/assembler.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/boundary.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/engine.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/pruner.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/summarizer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/types.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/validator.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/consolidation.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/context_cache.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/executors/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/executors/base.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/executors/factory.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/executors/remote.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/loop.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/audit.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/error_messages.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/error_types.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/models.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/registry.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/runtime.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/pipeline/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/pipeline/inference_stage.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/pipeline/response_stage.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/pipeline/types.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/planning/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/planning/models.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/planning/planner.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/planning/reflection.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/planning/strategies.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/streaming.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/base.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/circuit_breaker.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/clarify.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/code_exec.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/cronjob.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/delegate.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/filesystem.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/image_gen.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/knowledge.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/memory.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/message.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/notify.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/patch.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/process.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/registry.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/search.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/session_search.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/shell.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/skill_install.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/skills.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/task.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/todo.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/vision.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/web.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/workflow.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/app.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/bus/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/bus/events.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/bus/queue.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/bus/rate_limiter.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/cli.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/cron.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/dingtalk.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/discord.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/email.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/qqbot.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/qqbot_media.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/webhook.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/wecom.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/colors.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/evolution_cmd.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/i18n/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/i18n/en.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/i18n/zh.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/plugins_cmd.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/prompt.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/service.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/setup.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/status.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/config/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/config/default.yaml +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/config/loader.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evaluation/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evaluation/dataset.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evaluation/metrics.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evaluation/reporter.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evaluation/runner.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/engine.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/evolver.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/gate.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/recorder.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/scheduler.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/store.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/tools.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/types.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/validation.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/auth.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/editor.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/health.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/hooks.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/media.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/rate_limiter.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/router.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/server.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/session_context.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/session_policy.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/static/index.html +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/knowledge/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/knowledge/index.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/client.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/oauth.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/security.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/tool_adapter.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/transport.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/contradiction.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/forgetting.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/retrieval.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/reviewer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/store.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/tiers.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/types.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/vectors.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/credential_pool.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/inference.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/provider.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/anthropic_provider.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/openai_provider.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/openrouter_provider.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/rate_limiter.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/router.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/stub.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/tokenizer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/observability/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/observability/monitor.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/observability/spans.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/observability/telemetry.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/permissions/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/permissions/allowlist.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/permissions/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/context.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/errors.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/hooks.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/loader.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/manifest.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/sandbox.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/runtime_paths.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/scheduler/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/scheduler/delivery.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/scheduler/service.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/capabilities.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/guards.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/normalizer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/path_policy.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/risk_classifier.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/smart_approval.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/tokenizer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/tool_policy.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/session/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/session/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/skills/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/skills/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/skills/reviewer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/skills/store.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/storage/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/storage/backend.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/storage/sqlite.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tasks/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tasks/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tasks/models.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tasks/workflow.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tools/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tools/base.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/utils/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/utils/async_io.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/utils/text.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/scripts/install.sh +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/development/plan/SKILL.md +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/development/skill-creator/SKILL.md +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/productivity/summarize/SKILL.md +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/productivity/weather/SKILL.md +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/research/arxiv/SKILL.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: echo-agent
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A modular AI agent framework with multi-channel support
|
|
5
5
|
Author: Echo Agent contributors
|
|
6
6
|
License: MIT
|
|
@@ -55,6 +55,24 @@ Requires-Dist: opentelemetry-exporter-otlp>=1.20; extra == 'otel'
|
|
|
55
55
|
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'otel'
|
|
56
56
|
Provides-Extra: process
|
|
57
57
|
Requires-Dist: psutil>=5.9; extra == 'process'
|
|
58
|
+
Provides-Extra: skills
|
|
59
|
+
Requires-Dist: caldav>=1.3; extra == 'skills'
|
|
60
|
+
Requires-Dist: croniter>=1.4; extra == 'skills'
|
|
61
|
+
Requires-Dist: duckduckgo-search>=7.0; extra == 'skills'
|
|
62
|
+
Requires-Dist: edge-tts>=7.0; extra == 'skills'
|
|
63
|
+
Requires-Dist: faster-whisper>=1.0; extra == 'skills'
|
|
64
|
+
Requires-Dist: feedparser>=6.0; extra == 'skills'
|
|
65
|
+
Requires-Dist: icalendar>=5.0; extra == 'skills'
|
|
66
|
+
Requires-Dist: markdown>=3.6; extra == 'skills'
|
|
67
|
+
Requires-Dist: openpyxl>=3.1; extra == 'skills'
|
|
68
|
+
Requires-Dist: pillow>=10.0; extra == 'skills'
|
|
69
|
+
Requires-Dist: psutil>=5.9; extra == 'skills'
|
|
70
|
+
Requires-Dist: pymupdf>=1.24; extra == 'skills'
|
|
71
|
+
Requires-Dist: pytesseract>=0.3; extra == 'skills'
|
|
72
|
+
Requires-Dist: python-docx>=1.1; extra == 'skills'
|
|
73
|
+
Requires-Dist: python-pptx>=1.0; extra == 'skills'
|
|
74
|
+
Requires-Dist: pyyaml>=6.0; extra == 'skills'
|
|
75
|
+
Requires-Dist: trafilatura>=2.0; extra == 'skills'
|
|
58
76
|
Provides-Extra: tokenizers
|
|
59
77
|
Requires-Dist: tiktoken>=0.7; extra == 'tokenizers'
|
|
60
78
|
Provides-Extra: vector
|
|
@@ -76,9 +94,6 @@ Description-Content-Type: text/markdown
|
|
|
76
94
|
<br/>
|
|
77
95
|
|
|
78
96
|
[](https://pypi.org/project/echo-agent/)
|
|
79
|
-
[](https://pypi.org/project/echo-agent/)
|
|
80
|
-
[](https://github.com/fuyuxiang/echo-agent/actions/workflows/ci.yml)
|
|
81
|
-
[](LICENSE)
|
|
82
97
|
[](https://pepy.tech/project/echo-agent)
|
|
83
98
|
[](https://github.com/fuyuxiang/echo-agent)
|
|
84
99
|
|
|
@@ -202,9 +217,7 @@ PR 前请确保 lint 和测试通过(CI 会在 PR 上自动运行同样的检
|
|
|
202
217
|
**参与方向:** 通道适配器 · 内置工具 · MCP 集成 · 技能示例 · 评测数据集 · 文档完善 · 部署模板
|
|
203
218
|
|
|
204
219
|
**社区:**
|
|
205
|
-
- [
|
|
206
|
-
- [GitHub Issues](https://github.com/fuyuxiang/echo-agent/issues) — Bug 与新特性
|
|
207
|
-
- [QQ 群 47572014](https://qm.qq.com/q/JWOPDBNssw)
|
|
220
|
+
- QQ群:[47572014](https://qm.qq.com/q/JWOPDBNssw)
|
|
208
221
|
|
|
209
222
|
---
|
|
210
223
|
|
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
<br/>
|
|
12
12
|
|
|
13
13
|
[](https://pypi.org/project/echo-agent/)
|
|
14
|
-
[](https://pypi.org/project/echo-agent/)
|
|
15
|
-
[](https://github.com/fuyuxiang/echo-agent/actions/workflows/ci.yml)
|
|
16
|
-
[](LICENSE)
|
|
17
14
|
[](https://pepy.tech/project/echo-agent)
|
|
18
15
|
[](https://github.com/fuyuxiang/echo-agent)
|
|
19
16
|
|
|
@@ -137,9 +134,7 @@ PR 前请确保 lint 和测试通过(CI 会在 PR 上自动运行同样的检
|
|
|
137
134
|
**参与方向:** 通道适配器 · 内置工具 · MCP 集成 · 技能示例 · 评测数据集 · 文档完善 · 部署模板
|
|
138
135
|
|
|
139
136
|
**社区:**
|
|
140
|
-
- [
|
|
141
|
-
- [GitHub Issues](https://github.com/fuyuxiang/echo-agent/issues) — Bug 与新特性
|
|
142
|
-
- [QQ 群 47572014](https://qm.qq.com/q/JWOPDBNssw)
|
|
137
|
+
- QQ群:[47572014](https://qm.qq.com/q/JWOPDBNssw)
|
|
143
138
|
|
|
144
139
|
---
|
|
145
140
|
|
|
@@ -87,6 +87,14 @@ class ConversationCompressor(ContextEngine):
|
|
|
87
87
|
tokens_before = self.estimate_tokens(messages)
|
|
88
88
|
working = list(messages)
|
|
89
89
|
|
|
90
|
+
# Phase 0: Strip media_refs — they are lightweight session pointers
|
|
91
|
+
# that the summarizer/boundary stages should not see. The text
|
|
92
|
+
# content stays, so the LLM summary naturally captures "user sent
|
|
93
|
+
# an image" without carrying the ref metadata forward.
|
|
94
|
+
for msg in working:
|
|
95
|
+
if "media_refs" in msg:
|
|
96
|
+
del msg["media_refs"]
|
|
97
|
+
|
|
90
98
|
# Phase 1: Tool output pruning
|
|
91
99
|
pruned_count = 0
|
|
92
100
|
if self._pruner:
|
|
@@ -211,35 +211,59 @@ class ContextBuilder:
|
|
|
211
211
|
expiry-prone CDN URLs; on failure we fall back to the original URL so the
|
|
212
212
|
message is never dropped. Non-image attachments (file/video/audio) are not
|
|
213
213
|
downloaded — the model cannot consume their bytes, so we only reference them
|
|
214
|
-
by name/URL and skip the wasted I/O.
|
|
214
|
+
by name/URL and skip the wasted I/O.
|
|
215
|
+
|
|
216
|
+
If the media carries an AES key (WeChat CDN encryption), the downloaded bytes
|
|
217
|
+
are decrypted in-place before being handed to the model."""
|
|
215
218
|
resolved: list[dict[str, str]] = []
|
|
216
|
-
download_targets: list[tuple[int, str]] = []
|
|
219
|
+
download_targets: list[tuple[int, str, str]] = []
|
|
217
220
|
for idx, block in enumerate(items):
|
|
218
221
|
btype = getattr(block.type, "value", str(block.type))
|
|
219
222
|
url = block.url
|
|
223
|
+
meta = getattr(block, "metadata", None) or {}
|
|
224
|
+
aes_key = meta.get("aes_key", "")
|
|
220
225
|
entry = {
|
|
221
226
|
"type": btype,
|
|
222
227
|
"url": url,
|
|
223
228
|
"mime_type": getattr(block, "mime_type", "") or "",
|
|
224
229
|
"name": self._block_name(block),
|
|
230
|
+
"aes_key": aes_key,
|
|
231
|
+
"original_url": url,
|
|
225
232
|
}
|
|
226
233
|
resolved.append(entry)
|
|
227
234
|
if btype == "image" and url.startswith(("http://", "https://")):
|
|
228
|
-
download_targets.append((idx, url))
|
|
235
|
+
download_targets.append((idx, url, aes_key))
|
|
229
236
|
|
|
230
237
|
if download_targets:
|
|
231
238
|
cache = self._get_media_cache()
|
|
232
239
|
results = await asyncio.gather(
|
|
233
|
-
*(cache.download(url, channel or "inbound") for _, url in download_targets),
|
|
240
|
+
*(cache.download(url, channel or "inbound") for _, url, _ in download_targets),
|
|
234
241
|
return_exceptions=True,
|
|
235
242
|
)
|
|
236
|
-
for (idx, url), result in zip(download_targets, results):
|
|
243
|
+
for (idx, url, aes_key), result in zip(download_targets, results):
|
|
237
244
|
if isinstance(result, Exception):
|
|
238
245
|
logger.warning("Inbound media download failed, using original URL: {}", result)
|
|
239
246
|
elif result:
|
|
247
|
+
if aes_key:
|
|
248
|
+
result = self._decrypt_media_file(result, aes_key)
|
|
240
249
|
resolved[idx]["url"] = str(result)
|
|
241
250
|
return resolved
|
|
242
251
|
|
|
252
|
+
@staticmethod
|
|
253
|
+
def _decrypt_media_file(path: Path, aes_key_b64: str) -> Path:
|
|
254
|
+
"""Decrypt an AES-128-ECB encrypted media file in-place."""
|
|
255
|
+
from echo_agent.channels.weixin import _aes128_ecb_decrypt, _parse_aes_key
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
key = _parse_aes_key(aes_key_b64)
|
|
259
|
+
ciphertext = path.read_bytes()
|
|
260
|
+
plaintext = _aes128_ecb_decrypt(ciphertext, key)
|
|
261
|
+
path.write_bytes(plaintext)
|
|
262
|
+
logger.debug("Decrypted media file: {}", path.name)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.warning("Media decryption failed for {}: {}", path.name, e)
|
|
265
|
+
return path
|
|
266
|
+
|
|
243
267
|
def build_system_prompt(
|
|
244
268
|
self,
|
|
245
269
|
memory_context: str = "",
|
|
@@ -287,6 +311,9 @@ class ContextBuilder:
|
|
|
287
311
|
chat_id: str | None = None,
|
|
288
312
|
system_prompt: str = "",
|
|
289
313
|
retrieval_context: str = "",
|
|
314
|
+
history_image_ttl_minutes: int = 30,
|
|
315
|
+
history_image_limit: int = 4,
|
|
316
|
+
history_image_skip_if_current: bool = True,
|
|
290
317
|
) -> list[dict[str, Any]]:
|
|
291
318
|
runtime = self._runtime_context(channel, chat_id)
|
|
292
319
|
user_content = current_message
|
|
@@ -299,9 +326,18 @@ class ContextBuilder:
|
|
|
299
326
|
messages: list[dict[str, Any]] = []
|
|
300
327
|
if system_prompt:
|
|
301
328
|
messages.append({"role": "system", "content": system_prompt})
|
|
302
|
-
messages.extend(history)
|
|
303
329
|
|
|
304
330
|
normalized = self._normalize_media(media)
|
|
331
|
+
has_current_image = any(item.get("type") == "image" for item in normalized)
|
|
332
|
+
|
|
333
|
+
enriched_history = self._inject_history_images(
|
|
334
|
+
history,
|
|
335
|
+
ttl_minutes=history_image_ttl_minutes,
|
|
336
|
+
limit=history_image_limit,
|
|
337
|
+
skip=has_current_image and history_image_skip_if_current,
|
|
338
|
+
)
|
|
339
|
+
messages.extend(enriched_history)
|
|
340
|
+
|
|
305
341
|
if normalized:
|
|
306
342
|
content_parts: list[dict[str, Any]] = [{"type": "text", "text": merged_user}]
|
|
307
343
|
file_notes: list[str] = []
|
|
@@ -316,12 +352,8 @@ class ContextBuilder:
|
|
|
316
352
|
if image_url:
|
|
317
353
|
content_parts.append({"type": "image_url", "image_url": {"url": image_url}})
|
|
318
354
|
else:
|
|
319
|
-
# 图片本地缓存已失效/不可读:不要静默丢弃,降级为文本引用,
|
|
320
|
-
# 让模型至少知道用户发过一张图。
|
|
321
355
|
file_notes.append(f"[附件] 类型=image 名称={name} 路径={url}")
|
|
322
356
|
else:
|
|
323
|
-
# 非图片附件(文件/视频/音频)模型无法直接看图,改为文本引用,
|
|
324
|
-
# 给出类型、名称和本地路径,避免被误当成图片塞进 image_url。
|
|
325
357
|
file_notes.append(f"[附件] 类型={mtype} 名称={name} 路径={url}")
|
|
326
358
|
if file_notes:
|
|
327
359
|
content_parts[0]["text"] = merged_user + "\n\n" + "\n".join(file_notes)
|
|
@@ -330,6 +362,113 @@ class ContextBuilder:
|
|
|
330
362
|
messages.append({"role": "user", "content": merged_user})
|
|
331
363
|
return messages
|
|
332
364
|
|
|
365
|
+
def _inject_history_images(
|
|
366
|
+
self,
|
|
367
|
+
history: list[dict[str, Any]],
|
|
368
|
+
ttl_minutes: int = 30,
|
|
369
|
+
limit: int = 4,
|
|
370
|
+
skip: bool = False,
|
|
371
|
+
) -> list[dict[str, Any]]:
|
|
372
|
+
"""Enrich history messages that carry ``media_refs`` with image content.
|
|
373
|
+
|
|
374
|
+
Returns a shallow copy of *history* where qualifying user messages have
|
|
375
|
+
their ``content`` replaced by a multimodal list. The original dicts are
|
|
376
|
+
not mutated. When a cached file is missing, attempts to re-download from
|
|
377
|
+
the original URL (and decrypt if an AES key is stored). Expired or
|
|
378
|
+
unrecoverable images degrade to a text placeholder."""
|
|
379
|
+
if skip or limit <= 0:
|
|
380
|
+
if skip:
|
|
381
|
+
logger.debug("Skipping history image injection (current turn has images)")
|
|
382
|
+
return history
|
|
383
|
+
|
|
384
|
+
import time
|
|
385
|
+
|
|
386
|
+
now = time.time()
|
|
387
|
+
cutoff = now - ttl_minutes * 60
|
|
388
|
+
|
|
389
|
+
collected: list[tuple[int, list[dict[str, Any]]]] = []
|
|
390
|
+
total = 0
|
|
391
|
+
for idx in range(len(history) - 1, -1, -1):
|
|
392
|
+
if total >= limit:
|
|
393
|
+
break
|
|
394
|
+
msg = history[idx]
|
|
395
|
+
if msg.get("role") != "user":
|
|
396
|
+
continue
|
|
397
|
+
refs = msg.get("media_refs")
|
|
398
|
+
if not refs:
|
|
399
|
+
continue
|
|
400
|
+
parts: list[dict[str, Any]] = []
|
|
401
|
+
for ref in refs:
|
|
402
|
+
ts = ref.get("timestamp", 0)
|
|
403
|
+
if ts < cutoff:
|
|
404
|
+
age_min = (now - ts) / 60
|
|
405
|
+
logger.debug(
|
|
406
|
+
"History image expired ({:.0f}m old, TTL={}m): {}",
|
|
407
|
+
age_min, ttl_minutes, ref.get("cache_path", "?"),
|
|
408
|
+
)
|
|
409
|
+
continue
|
|
410
|
+
data_url = self._resolve_history_image(ref)
|
|
411
|
+
age_min = int((now - ts) / 60)
|
|
412
|
+
if data_url:
|
|
413
|
+
parts.append({"type": "image_url", "image_url": {"url": data_url}})
|
|
414
|
+
parts.append({
|
|
415
|
+
"type": "text",
|
|
416
|
+
"text": f"[历史图片,来自{age_min}分钟前]",
|
|
417
|
+
})
|
|
418
|
+
logger.debug("Injected history image ({} min old)", age_min)
|
|
419
|
+
else:
|
|
420
|
+
parts.append({"type": "text", "text": "[该图片已过期,无法显示]"})
|
|
421
|
+
logger.info(
|
|
422
|
+
"History image unavailable (cache={}, url={})",
|
|
423
|
+
ref.get("cache_path", ""), ref.get("original_url", ""),
|
|
424
|
+
)
|
|
425
|
+
total += 1
|
|
426
|
+
if total >= limit:
|
|
427
|
+
break
|
|
428
|
+
if parts:
|
|
429
|
+
collected.append((idx, parts))
|
|
430
|
+
|
|
431
|
+
if not collected:
|
|
432
|
+
return history
|
|
433
|
+
|
|
434
|
+
logger.debug("Injecting {} history image(s) into {} message(s)", total, len(collected))
|
|
435
|
+
enriched = list(history)
|
|
436
|
+
for idx, image_parts in collected:
|
|
437
|
+
orig = enriched[idx]
|
|
438
|
+
text = orig.get("content", "")
|
|
439
|
+
if isinstance(text, list):
|
|
440
|
+
continue
|
|
441
|
+
enriched[idx] = {
|
|
442
|
+
**orig,
|
|
443
|
+
"content": [{"type": "text", "text": text}] + image_parts,
|
|
444
|
+
}
|
|
445
|
+
return enriched
|
|
446
|
+
|
|
447
|
+
def _resolve_history_image(self, ref: dict[str, Any]) -> str | None:
|
|
448
|
+
"""Try to load a history image: cache first, then fallback re-download."""
|
|
449
|
+
cache_path = ref.get("cache_path", "")
|
|
450
|
+
if cache_path:
|
|
451
|
+
data_url = self._local_image_to_data_url(cache_path)
|
|
452
|
+
if data_url:
|
|
453
|
+
return data_url
|
|
454
|
+
|
|
455
|
+
original_url = ref.get("original_url", "")
|
|
456
|
+
if not original_url:
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
cache = self._get_media_cache()
|
|
460
|
+
cached = cache.get_cached(original_url)
|
|
461
|
+
if cached and cached.exists():
|
|
462
|
+
aes_key = ref.get("aes_key", "")
|
|
463
|
+
if aes_key:
|
|
464
|
+
self._decrypt_media_file(cached, aes_key)
|
|
465
|
+
data_url = self._local_image_to_data_url(str(cached))
|
|
466
|
+
if data_url:
|
|
467
|
+
logger.debug("Recovered history image from cache lookup: {}", cached.name)
|
|
468
|
+
return data_url
|
|
469
|
+
|
|
470
|
+
return None
|
|
471
|
+
|
|
333
472
|
@staticmethod
|
|
334
473
|
def _normalize_media(media: Any) -> list[dict[str, str]]:
|
|
335
474
|
"""Accept either a list of bare URL strings (legacy) or type-aware dicts."""
|
|
@@ -127,7 +127,18 @@ class ContextStage:
|
|
|
127
127
|
session.messages = session.messages[:session.last_consolidated] + result.messages
|
|
128
128
|
await self._sessions.save(session)
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
media_items = event.media_items
|
|
131
|
+
resolved_media = (
|
|
132
|
+
await self._context_builder.resolve_inbound_media(media_items, event.channel)
|
|
133
|
+
if media_items
|
|
134
|
+
else None
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
media_refs = self._build_media_refs(resolved_media) if resolved_media else None
|
|
138
|
+
if media_refs:
|
|
139
|
+
session.add_message("user", event.text, media_refs=media_refs)
|
|
140
|
+
else:
|
|
141
|
+
session.add_message("user", event.text)
|
|
131
142
|
|
|
132
143
|
retrieval_parts: list[str] = []
|
|
133
144
|
if self._config.memory.enabled:
|
|
@@ -159,13 +170,7 @@ class ContextStage:
|
|
|
159
170
|
|
|
160
171
|
retrieval = "\n\n".join(retrieval_parts)
|
|
161
172
|
|
|
162
|
-
|
|
163
|
-
resolved_media = (
|
|
164
|
-
await self._context_builder.resolve_inbound_media(media_items, event.channel)
|
|
165
|
-
if media_items
|
|
166
|
-
else None
|
|
167
|
-
)
|
|
168
|
-
|
|
173
|
+
session_cfg = self._config.session
|
|
169
174
|
messages = self._context_builder.build_messages(
|
|
170
175
|
history=history,
|
|
171
176
|
current_message=event.text,
|
|
@@ -174,6 +179,9 @@ class ContextStage:
|
|
|
174
179
|
chat_id=event.chat_id,
|
|
175
180
|
system_prompt=system_prompt,
|
|
176
181
|
retrieval_context=retrieval,
|
|
182
|
+
history_image_ttl_minutes=session_cfg.history_image_ttl_minutes,
|
|
183
|
+
history_image_limit=session_cfg.history_image_limit,
|
|
184
|
+
history_image_skip_if_current=session_cfg.history_image_skip_if_current,
|
|
177
185
|
)
|
|
178
186
|
|
|
179
187
|
# tool_defs already computed above for capability derivation.
|
|
@@ -192,9 +200,13 @@ class ContextStage:
|
|
|
192
200
|
# information — injecting it just burns prompt tokens.
|
|
193
201
|
if execution_plan and len(execution_plan.steps) > 1:
|
|
194
202
|
plan_context = execution_plan.to_prompt()
|
|
195
|
-
messages[-1]["content"]
|
|
196
|
-
|
|
197
|
-
|
|
203
|
+
last_content = messages[-1]["content"]
|
|
204
|
+
if isinstance(last_content, list):
|
|
205
|
+
last_content[0]["text"] += f"\n\n[Plan]\n{plan_context}"
|
|
206
|
+
else:
|
|
207
|
+
messages[-1]["content"] = (
|
|
208
|
+
last_content + f"\n\n[Plan]\n{plan_context}"
|
|
209
|
+
)
|
|
198
210
|
except Exception as e:
|
|
199
211
|
logger.debug("Planning failed, proceeding without plan: {}", e)
|
|
200
212
|
|
|
@@ -213,6 +225,31 @@ class ContextStage:
|
|
|
213
225
|
stream_publisher=stream_publisher,
|
|
214
226
|
)
|
|
215
227
|
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _build_media_refs(resolved_media: list[dict[str, str]]) -> list[dict[str, Any]]:
|
|
230
|
+
"""Extract lightweight image references from resolved media for session storage."""
|
|
231
|
+
import time
|
|
232
|
+
|
|
233
|
+
from echo_agent.session.media_ref import MediaRef
|
|
234
|
+
|
|
235
|
+
refs: list[dict[str, Any]] = []
|
|
236
|
+
now = time.time()
|
|
237
|
+
for item in resolved_media:
|
|
238
|
+
if item.get("type") != "image":
|
|
239
|
+
continue
|
|
240
|
+
url = item.get("url", "")
|
|
241
|
+
if not url:
|
|
242
|
+
continue
|
|
243
|
+
is_local = not url.startswith(("http://", "https://", "data:"))
|
|
244
|
+
refs.append(MediaRef(
|
|
245
|
+
cache_path=url if is_local else "",
|
|
246
|
+
original_url=item.get("original_url", "") or (url if not is_local else ""),
|
|
247
|
+
mime_type=item.get("mime_type", ""),
|
|
248
|
+
timestamp=now,
|
|
249
|
+
aes_key=item.get("aes_key", ""),
|
|
250
|
+
).to_dict())
|
|
251
|
+
return refs
|
|
252
|
+
|
|
216
253
|
def _infer_task_type(self, text: str) -> str:
|
|
217
254
|
lower = text.lower()
|
|
218
255
|
for task_type, markers in self._TASK_MARKERS.items():
|
|
@@ -115,8 +115,8 @@ def discover_tools(
|
|
|
115
115
|
from echo_agent.agent.tools.vision import VisionTool
|
|
116
116
|
tools.append(VisionTool(provider=provider, workspace=ws))
|
|
117
117
|
|
|
118
|
-
_try_register_image_gen(tools, config)
|
|
119
|
-
_try_register_tts(tools, config, ws)
|
|
118
|
+
_try_register_image_gen(tools, config, provider)
|
|
119
|
+
_try_register_tts(tools, config, ws, provider)
|
|
120
120
|
|
|
121
121
|
if session_manager:
|
|
122
122
|
from echo_agent.agent.tools.session_search import SessionSearchTool
|
|
@@ -148,20 +148,98 @@ def discover_tools(
|
|
|
148
148
|
return tools
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
def
|
|
151
|
+
def _unwrap_provider(provider: LLMProvider | None) -> LLMProvider | None:
|
|
152
|
+
"""Unwrap decorator layers (RateLimitedProvider, _PooledProvider) to get the real provider."""
|
|
153
|
+
if provider is None:
|
|
154
|
+
return None
|
|
155
|
+
inner = provider
|
|
156
|
+
while hasattr(inner, "_inner"):
|
|
157
|
+
inner = inner._inner
|
|
158
|
+
return inner
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _is_openai_compatible_provider(provider: LLMProvider | None) -> bool:
|
|
162
|
+
"""Check if provider uses the OpenAI-compatible API (images/audio endpoints available)."""
|
|
163
|
+
from echo_agent.models.providers.openai_provider import OpenAIProvider
|
|
164
|
+
inner = _unwrap_provider(provider)
|
|
165
|
+
return isinstance(inner, OpenAIProvider)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _infer_image_model(api_base: str) -> str:
|
|
169
|
+
"""Infer image generation model name from API base URL."""
|
|
170
|
+
base = (api_base or "").lower()
|
|
171
|
+
if "minimax" in base:
|
|
172
|
+
return "image-01"
|
|
173
|
+
if "dashscope" in base or "aliyun" in base:
|
|
174
|
+
return "wanx-v1"
|
|
175
|
+
if "zhipu" in base or "bigmodel" in base:
|
|
176
|
+
return "cogview-3"
|
|
177
|
+
return "dall-e-3"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _infer_tts_model(api_base: str) -> str:
|
|
181
|
+
"""Infer TTS model name from API base URL."""
|
|
182
|
+
base = (api_base or "").lower()
|
|
183
|
+
if "minimax" in base:
|
|
184
|
+
return "speech-02"
|
|
185
|
+
if "dashscope" in base or "aliyun" in base:
|
|
186
|
+
return "cosyvoice-v1"
|
|
187
|
+
return "tts-1"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _try_register_image_gen(tools: list[Tool], config: Config, provider: LLMProvider | None = None) -> None:
|
|
152
191
|
ig = getattr(config.tools, "image_gen", None)
|
|
153
|
-
|
|
192
|
+
explicit_key = getattr(ig, "api_key", "") if ig else ""
|
|
193
|
+
api_base = getattr(ig, "api_base", "") if ig else ""
|
|
194
|
+
model = getattr(ig, "model", "") if ig else ""
|
|
195
|
+
|
|
196
|
+
if explicit_key:
|
|
197
|
+
# User explicitly configured image_gen — use their settings as-is
|
|
198
|
+
api_key = explicit_key
|
|
199
|
+
elif _is_openai_compatible_provider(provider):
|
|
200
|
+
# Fallback only for OpenAI-compatible providers — use unwrapped to get real key/base
|
|
201
|
+
real = _unwrap_provider(provider)
|
|
202
|
+
api_key = getattr(real, "api_key", "")
|
|
203
|
+
if not api_base:
|
|
204
|
+
api_base = getattr(real, "api_base", "")
|
|
205
|
+
# Reset default model so we infer from api_base
|
|
206
|
+
model = ""
|
|
207
|
+
else:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if not api_key:
|
|
154
211
|
return
|
|
212
|
+
|
|
213
|
+
# Infer model from api_base if not explicitly set by user
|
|
214
|
+
if not model or (not explicit_key and model == "dall-e-3"):
|
|
215
|
+
model = _infer_image_model(api_base)
|
|
216
|
+
|
|
155
217
|
from echo_agent.agent.tools.image_gen import ImageGenTool
|
|
156
|
-
tools.append(ImageGenTool(
|
|
157
|
-
api_key=ig.api_key,
|
|
158
|
-
api_base=getattr(ig, "api_base", ""),
|
|
159
|
-
model=getattr(ig, "model", "dall-e-3"),
|
|
160
|
-
))
|
|
218
|
+
tools.append(ImageGenTool(api_key=api_key, api_base=api_base, model=model))
|
|
161
219
|
|
|
162
220
|
|
|
163
|
-
def _try_register_tts(tools: list[Tool], config: Config, ws: str) -> None:
|
|
221
|
+
def _try_register_tts(tools: list[Tool], config: Config, ws: str, provider: LLMProvider | None = None) -> None:
|
|
164
222
|
from echo_agent.agent.tools.tts import TTSTool
|
|
165
223
|
tts_cfg = getattr(config.tools, "tts", None)
|
|
166
224
|
openai_key = getattr(tts_cfg, "openai_api_key", "") if tts_cfg else ""
|
|
167
|
-
|
|
225
|
+
openai_base = ""
|
|
226
|
+
default_backend = getattr(tts_cfg, "default_backend", "edge") if tts_cfg else "edge"
|
|
227
|
+
default_voice = getattr(tts_cfg, "default_voice", "") if tts_cfg else ""
|
|
228
|
+
|
|
229
|
+
# Fallback only for OpenAI-compatible providers
|
|
230
|
+
if not openai_key and _is_openai_compatible_provider(provider):
|
|
231
|
+
real = _unwrap_provider(provider)
|
|
232
|
+
openai_key = getattr(real, "api_key", "")
|
|
233
|
+
openai_base = getattr(real, "api_base", "")
|
|
234
|
+
|
|
235
|
+
# Infer TTS model from api_base
|
|
236
|
+
tts_model = _infer_tts_model(openai_base)
|
|
237
|
+
|
|
238
|
+
tools.append(TTSTool(
|
|
239
|
+
workspace=ws,
|
|
240
|
+
openai_api_key=openai_key,
|
|
241
|
+
openai_api_base=openai_base,
|
|
242
|
+
tts_model=tts_model,
|
|
243
|
+
default_backend=default_backend,
|
|
244
|
+
default_voice=default_voice,
|
|
245
|
+
))
|
|
@@ -23,14 +23,18 @@ class TTSTool(Tool):
|
|
|
23
23
|
}
|
|
24
24
|
timeout_seconds = 60
|
|
25
25
|
|
|
26
|
-
def __init__(self, workspace: str, openai_api_key: str = ""):
|
|
26
|
+
def __init__(self, workspace: str, openai_api_key: str = "", openai_api_base: str = "", tts_model: str = "tts-1", default_backend: str = "", default_voice: str = ""):
|
|
27
27
|
self._workspace = Path(workspace)
|
|
28
28
|
self._openai_key = openai_api_key
|
|
29
|
+
self._openai_base = (openai_api_base or "https://api.openai.com/v1").rstrip("/")
|
|
30
|
+
self._tts_model = tts_model
|
|
31
|
+
self._default_backend = default_backend or "edge"
|
|
32
|
+
self._default_voice = default_voice
|
|
29
33
|
|
|
30
34
|
async def execute(self, params: dict[str, Any], ctx: ToolExecutionContext | None = None) -> ToolResult:
|
|
31
35
|
text = params["text"]
|
|
32
|
-
backend = params.get("backend",
|
|
33
|
-
voice = params.get("voice", "")
|
|
36
|
+
backend = params.get("backend", self._default_backend)
|
|
37
|
+
voice = params.get("voice", self._default_voice or "")
|
|
34
38
|
output_path = params.get("output_path", "")
|
|
35
39
|
|
|
36
40
|
if not output_path:
|
|
@@ -59,9 +63,9 @@ class TTSTool(Tool):
|
|
|
59
63
|
return ToolResult(success=False, error="OpenAI API key not configured for TTS")
|
|
60
64
|
|
|
61
65
|
import aiohttp
|
|
62
|
-
url = "
|
|
66
|
+
url = f"{self._openai_base}/audio/speech"
|
|
63
67
|
headers = {"Authorization": f"Bearer {self._openai_key}", "Content-Type": "application/json"}
|
|
64
|
-
body = {"model":
|
|
68
|
+
body = {"model": self._tts_model, "input": text, "voice": voice, "response_format": "mp3"}
|
|
65
69
|
|
|
66
70
|
try:
|
|
67
71
|
async with aiohttp.ClientSession() as session:
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
5
6
|
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
from typing import Any
|
|
@@ -112,11 +114,16 @@ class BaseChannel(ABC):
|
|
|
112
114
|
content_blocks = [ContentBlock(type=ContentType.TEXT, text=text)]
|
|
113
115
|
for item in (media or []):
|
|
114
116
|
name = item.get("name", "")
|
|
117
|
+
meta: dict[str, Any] = {}
|
|
118
|
+
if name:
|
|
119
|
+
meta["name"] = name
|
|
120
|
+
if item.get("aes_key"):
|
|
121
|
+
meta["aes_key"] = item["aes_key"]
|
|
115
122
|
content_blocks.append(ContentBlock(
|
|
116
123
|
type=ContentType(item.get("type", "file")),
|
|
117
124
|
url=item.get("url", ""),
|
|
118
125
|
mime_type=item.get("mime_type", ""),
|
|
119
|
-
metadata=
|
|
126
|
+
metadata=meta,
|
|
120
127
|
))
|
|
121
128
|
|
|
122
129
|
return InboundEvent(
|
|
@@ -173,6 +180,40 @@ class BaseChannel(ABC):
|
|
|
173
180
|
def is_running(self) -> bool:
|
|
174
181
|
return self._running
|
|
175
182
|
|
|
183
|
+
_media_cache_root: Path | None = None
|
|
184
|
+
|
|
185
|
+
async def _resolve_media_to_cache(
|
|
186
|
+
self,
|
|
187
|
+
source_id: str,
|
|
188
|
+
platform: str,
|
|
189
|
+
fetch: Callable[[], Awaitable[bytes]],
|
|
190
|
+
suffix: str = ".jpg",
|
|
191
|
+
) -> str | None:
|
|
192
|
+
"""Download media via a channel-provided *fetch* callback and cache locally.
|
|
193
|
+
|
|
194
|
+
Returns the absolute path string on success, ``None`` on any failure.
|
|
195
|
+
The caller decides how to degrade (skip the image, insert a placeholder, etc.).
|
|
196
|
+
"""
|
|
197
|
+
root = self._media_cache_root or (Path.home() / ".echo-agent" / "data" / "media_cache")
|
|
198
|
+
cache_dir = root / platform
|
|
199
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
url_hash = hashlib.sha256(source_id.encode()).hexdigest()[:16]
|
|
201
|
+
target = cache_dir / f"{url_hash}{suffix}"
|
|
202
|
+
if target.exists():
|
|
203
|
+
target.touch()
|
|
204
|
+
return str(target)
|
|
205
|
+
try:
|
|
206
|
+
data = await fetch()
|
|
207
|
+
if not data:
|
|
208
|
+
logger.warning("Empty media response for {} on {}", source_id[:60], platform)
|
|
209
|
+
return None
|
|
210
|
+
target.write_bytes(data)
|
|
211
|
+
logger.debug("Cached channel media: {} → {}", source_id[:60], target.name)
|
|
212
|
+
return str(target)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.warning("Channel media download failed for {} on {}: {}", source_id[:60], platform, e)
|
|
215
|
+
return None
|
|
216
|
+
|
|
176
217
|
async def transcribe_audio(self, file_path: str | Path) -> str:
|
|
177
218
|
"""Transcribe audio file via Groq Whisper API."""
|
|
178
219
|
api_key = self.transcription_api_key
|
|
@@ -157,7 +157,11 @@ class FeishuChannel(BaseChannel):
|
|
|
157
157
|
content = json.loads(message.get("content", "{}"))
|
|
158
158
|
image_key = content.get("image_key", "")
|
|
159
159
|
if image_key:
|
|
160
|
-
|
|
160
|
+
local_path = await self._download_feishu_image(image_key, msg_id)
|
|
161
|
+
if local_path:
|
|
162
|
+
media.append({"type": "image", "url": local_path})
|
|
163
|
+
else:
|
|
164
|
+
logger.warning("Feishu image download failed, skipping: {}", image_key[:30])
|
|
161
165
|
except json.JSONDecodeError:
|
|
162
166
|
pass
|
|
163
167
|
|
|
@@ -188,6 +192,24 @@ class FeishuChannel(BaseChannel):
|
|
|
188
192
|
metadata={"chat_type": event.get("chat_type", ""), "receive_id_type": "chat_id"},
|
|
189
193
|
)
|
|
190
194
|
|
|
195
|
+
async def _download_feishu_image(self, image_key: str, msg_id: str) -> str | None:
|
|
196
|
+
"""Download a Feishu image by image_key via the message resource API."""
|
|
197
|
+
async def fetch() -> bytes:
|
|
198
|
+
await self._ensure_tenant_token()
|
|
199
|
+
url = (
|
|
200
|
+
f"{_API_BASE}/im/v1/messages/{msg_id}"
|
|
201
|
+
f"/resources/{image_key}?type=image"
|
|
202
|
+
)
|
|
203
|
+
headers = {"Authorization": f"Bearer {self._tenant_token}"}
|
|
204
|
+
if not self._session:
|
|
205
|
+
raise RuntimeError("no session")
|
|
206
|
+
async with self._session.get(url, headers=headers) as resp:
|
|
207
|
+
if resp.status != 200:
|
|
208
|
+
raise RuntimeError(f"Feishu resource download failed ({resp.status})")
|
|
209
|
+
return await resp.read()
|
|
210
|
+
|
|
211
|
+
return await self._resolve_media_to_cache(image_key, "feishu", fetch, suffix=".jpg")
|
|
212
|
+
|
|
191
213
|
# ── Encryption ───────────────────────────────────────────────────────────
|
|
192
214
|
|
|
193
215
|
def _decrypt(self, encrypted: str) -> dict[str, Any] | None:
|