echo-agent 0.2.0__tar.gz → 0.2.2__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.2}/PKG-INFO +23 -7
- {echo_agent-0.2.0 → echo_agent-0.2.2}/README.md +1 -6
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/compression/compressor.py +8 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/context.py +149 -10
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/pipeline/context_stage.py +48 -11
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/__init__.py +106 -11
- echo_agent-0.2.2/echo_agent/agent/tools/image_gen_fal.py +231 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/tts.py +9 -5
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/base.py +57 -1
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/dingtalk.py +2 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/email.py +2 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/feishu.py +25 -1
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/matrix.py +31 -2
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/qqbot.py +2 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/slack.py +24 -4
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/telegram.py +28 -2
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/webhook.py +2 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/wecom.py +2 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/weixin.py +23 -6
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/whatsapp.py +31 -3
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/i18n/en.py +8 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/i18n/zh.py +8 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/setup.py +32 -10
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/config/schema.py +12 -1
- echo_agent-0.2.2/echo_agent/dependencies/__init__.py +37 -0
- echo_agent-0.2.2/echo_agent/dependencies/cli.py +167 -0
- echo_agent-0.2.2/echo_agent/dependencies/lazy_deps.py +489 -0
- echo_agent-0.2.2/echo_agent/dependencies/skill_require.py +133 -0
- echo_agent-0.2.2/echo_agent/gateway/api/__init__.py +50 -0
- echo_agent-0.2.2/echo_agent/gateway/api/channels.py +53 -0
- echo_agent-0.2.2/echo_agent/gateway/api/config.py +85 -0
- echo_agent-0.2.2/echo_agent/gateway/api/knowledge.py +136 -0
- echo_agent-0.2.2/echo_agent/gateway/api/memory.py +137 -0
- echo_agent-0.2.2/echo_agent/gateway/api/skills.py +66 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/server.py +4 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/memory/consolidator.py +14 -1
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/providers/bedrock_provider.py +28 -2
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/providers/format_utils.py +33 -3
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/providers/gemini_provider.py +24 -2
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/security/capabilities.py +2 -2
- echo_agent-0.2.2/echo_agent/session/media_ref.py +42 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/pyproject.toml +22 -2
- echo_agent-0.2.2/skills/creative/excel-author/SKILL.md +88 -0
- echo_agent-0.2.2/skills/creative/excel-author/scripts/create_xlsx.py +97 -0
- echo_agent-0.2.2/skills/creative/image-gen/SKILL.md +74 -0
- echo_agent-0.2.2/skills/creative/image-gen/scripts/generate_image.py +73 -0
- echo_agent-0.2.2/skills/creative/meme-gen/SKILL.md +66 -0
- echo_agent-0.2.2/skills/creative/meme-gen/scripts/make_meme.py +121 -0
- echo_agent-0.2.2/skills/creative/ppt-author/SKILL.md +87 -0
- echo_agent-0.2.2/skills/creative/ppt-author/scripts/create_pptx.py +94 -0
- echo_agent-0.2.2/skills/development/code-runner/SKILL.md +71 -0
- echo_agent-0.2.2/skills/development/code-runner/scripts/safe_exec.py +133 -0
- echo_agent-0.2.2/skills/development/github-ops/SKILL.md +81 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/skills/development/skill-creator/scripts/init_skill.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/skills/development/skill-creator/scripts/package_skill.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/skills/development/skill-creator/scripts/quick_validate.py +0 -0
- echo_agent-0.2.2/skills/development/workflow-chain/SKILL.md +103 -0
- echo_agent-0.2.2/skills/development/workflow-chain/scripts/workflow_engine.py +100 -0
- echo_agent-0.2.2/skills/devops/docker-manage/SKILL.md +85 -0
- echo_agent-0.2.2/skills/devops/system-monitor/SKILL.md +77 -0
- echo_agent-0.2.2/skills/devops/system-monitor/scripts/system_check.py +110 -0
- echo_agent-0.2.2/skills/finance/finance-tracker/SKILL.md +66 -0
- echo_agent-0.2.2/skills/finance/finance-tracker/scripts/finance_manager.py +141 -0
- echo_agent-0.2.2/skills/finance/stocks/SKILL.md +75 -0
- echo_agent-0.2.2/skills/finance/stocks/scripts/market_query.py +99 -0
- echo_agent-0.2.2/skills/health/fitness-nutrition/SKILL.md +68 -0
- echo_agent-0.2.2/skills/health/fitness-nutrition/scripts/health_query.py +83 -0
- echo_agent-0.2.2/skills/learning/flashcards/SKILL.md +80 -0
- echo_agent-0.2.2/skills/learning/flashcards/scripts/flashcard_engine.py +170 -0
- echo_agent-0.2.2/skills/media/tts-voice/SKILL.md +82 -0
- echo_agent-0.2.2/skills/media/tts-voice/scripts/text_to_speech.py +55 -0
- echo_agent-0.2.2/skills/media/voice-note/SKILL.md +87 -0
- echo_agent-0.2.2/skills/media/voice-note/scripts/voice_process.py +68 -0
- echo_agent-0.2.2/skills/productivity/calendar/SKILL.md +64 -0
- echo_agent-0.2.2/skills/productivity/calendar/scripts/calendar_client.py +96 -0
- echo_agent-0.2.2/skills/productivity/daily-briefing/SKILL.md +69 -0
- echo_agent-0.2.2/skills/productivity/daily-briefing/scripts/generate_briefing.py +57 -0
- echo_agent-0.2.2/skills/productivity/email-assistant/SKILL.md +80 -0
- echo_agent-0.2.2/skills/productivity/email-assistant/scripts/email_client.py +99 -0
- echo_agent-0.2.2/skills/productivity/note-taking/SKILL.md +60 -0
- echo_agent-0.2.2/skills/productivity/note-taking/scripts/notes_manager.py +126 -0
- echo_agent-0.2.2/skills/productivity/notion-sync/SKILL.md +80 -0
- echo_agent-0.2.2/skills/productivity/notion-sync/scripts/notion_client.py +81 -0
- echo_agent-0.2.2/skills/productivity/ocr-document/SKILL.md +86 -0
- echo_agent-0.2.2/skills/productivity/ocr-document/scripts/extract_document.py +75 -0
- echo_agent-0.2.2/skills/productivity/reminder/SKILL.md +65 -0
- echo_agent-0.2.2/skills/productivity/reminder/scripts/reminder_store.py +112 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/skills/research/arxiv/scripts/search_arxiv.py +0 -0
- echo_agent-0.2.2/skills/research/deep-research/SKILL.md +74 -0
- echo_agent-0.2.2/skills/research/deep-research/scripts/research_report.py +95 -0
- echo_agent-0.2.2/skills/research/rss-watcher/SKILL.md +70 -0
- echo_agent-0.2.2/skills/research/rss-watcher/scripts/feed_monitor.py +100 -0
- echo_agent-0.2.2/skills/research/web-extract/SKILL.md +82 -0
- echo_agent-0.2.2/skills/research/web-extract/scripts/extract_url.py +38 -0
- echo_agent-0.2.2/skills/research/web-search/SKILL.md +80 -0
- echo_agent-0.2.2/skills/research/web-search/scripts/web_search.py +54 -0
- echo_agent-0.2.2/skills/utility/calculator/SKILL.md +91 -0
- echo_agent-0.2.2/skills/utility/calculator/scripts/calc.py +103 -0
- echo_agent-0.2.2/skills/utility/file-convert/SKILL.md +59 -0
- echo_agent-0.2.2/skills/utility/file-convert/scripts/convert.py +83 -0
- echo_agent-0.2.2/skills/utility/maps-poi/SKILL.md +77 -0
- echo_agent-0.2.2/skills/utility/maps-poi/scripts/geo_query.py +86 -0
- echo_agent-0.2.2/skills/utility/text-tools/SKILL.md +86 -0
- echo_agent-0.2.2/skills/utility/text-tools/scripts/text_process.py +94 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/.gitignore +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/LICENSE +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/__main__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/a2a/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/a2a/client.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/a2a/models.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/a2a/protocol.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/a2a/server.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/approval_gate.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/compression/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/compression/assembler.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/compression/boundary.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/compression/engine.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/compression/pruner.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/compression/summarizer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/compression/types.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/compression/validator.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/consolidation.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/context_cache.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/executors/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/executors/base.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/executors/factory.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/executors/remote.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/loop.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/audit.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/error_messages.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/error_types.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/models.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/registry.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/runtime.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/pipeline/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/pipeline/inference_stage.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/pipeline/response_stage.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/pipeline/types.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/planning/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/planning/models.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/planning/planner.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/planning/reflection.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/planning/strategies.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/streaming.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/base.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/circuit_breaker.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/clarify.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/code_exec.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/cronjob.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/delegate.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/filesystem.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/image_gen.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/knowledge.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/memory.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/message.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/notify.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/patch.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/process.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/registry.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/search.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/session_search.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/shell.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/skill_install.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/skills.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/task.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/todo.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/vision.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/web.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/agent/tools/workflow.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/app.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/bus/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/bus/events.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/bus/queue.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/bus/rate_limiter.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/cli.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/cron.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/discord.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/channels/qqbot_media.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/colors.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/evolution_cmd.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/i18n/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/plugins_cmd.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/prompt.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/service.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/cli/status.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/config/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/config/default.yaml +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/config/loader.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evaluation/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evaluation/dataset.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evaluation/metrics.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evaluation/reporter.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evaluation/runner.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evolution/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evolution/engine.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evolution/evolver.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evolution/gate.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evolution/recorder.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evolution/scheduler.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evolution/store.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evolution/tools.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evolution/types.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/evolution/validation.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/auth.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/editor.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/health.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/hooks.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/media.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/rate_limiter.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/router.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/session_context.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/session_policy.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/gateway/static/index.html +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/knowledge/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/knowledge/index.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/mcp/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/mcp/client.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/mcp/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/mcp/oauth.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/mcp/security.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/mcp/tool_adapter.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/mcp/transport.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/memory/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/memory/contradiction.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/memory/forgetting.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/memory/retrieval.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/memory/reviewer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/memory/store.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/memory/tiers.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/memory/types.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/memory/vectors.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/credential_pool.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/inference.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/provider.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/providers/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/providers/anthropic_provider.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/providers/openai_provider.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/providers/openrouter_provider.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/rate_limiter.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/router.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/stub.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/models/tokenizer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/observability/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/observability/monitor.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/observability/spans.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/observability/telemetry.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/permissions/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/permissions/allowlist.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/permissions/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/plugins/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/plugins/context.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/plugins/errors.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/plugins/hooks.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/plugins/loader.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/plugins/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/plugins/manifest.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/plugins/sandbox.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/runtime_paths.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/scheduler/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/scheduler/delivery.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/scheduler/service.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/security/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/security/guards.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/security/normalizer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/security/path_policy.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/security/risk_classifier.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/security/smart_approval.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/security/tokenizer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/security/tool_policy.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/session/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/session/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/skills/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/skills/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/skills/reviewer.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/skills/store.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/storage/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/storage/backend.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/storage/sqlite.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/tasks/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/tasks/manager.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/tasks/models.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/tasks/workflow.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/tools/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/tools/base.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/utils/__init__.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/utils/async_io.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/echo_agent/utils/text.py +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/scripts/install.sh +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/skills/development/plan/SKILL.md +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/skills/development/skill-creator/SKILL.md +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/skills/productivity/summarize/SKILL.md +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/skills/productivity/weather/SKILL.md +0 -0
- {echo_agent-0.2.0 → echo_agent-0.2.2}/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.2
|
|
4
4
|
Summary: A modular AI agent framework with multi-channel support
|
|
5
5
|
Author: Echo Agent contributors
|
|
6
6
|
License: MIT
|
|
@@ -25,6 +25,7 @@ Requires-Dist: anthropic>=0.40; extra == 'all'
|
|
|
25
25
|
Requires-Dist: boto3>=1.34; extra == 'all'
|
|
26
26
|
Requires-Dist: cryptography>=41.0; extra == 'all'
|
|
27
27
|
Requires-Dist: faiss-cpu>=1.7; extra == 'all'
|
|
28
|
+
Requires-Dist: fal-client>=0.5; extra == 'all'
|
|
28
29
|
Requires-Dist: google-generativeai>=0.8; extra == 'all'
|
|
29
30
|
Requires-Dist: openai>=1.30; extra == 'all'
|
|
30
31
|
Requires-Dist: psutil>=5.9; extra == 'all'
|
|
@@ -45,6 +46,8 @@ Provides-Extra: dev
|
|
|
45
46
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
46
47
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
47
48
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
49
|
+
Provides-Extra: fal
|
|
50
|
+
Requires-Dist: fal-client>=0.5; extra == 'fal'
|
|
48
51
|
Provides-Extra: gemini
|
|
49
52
|
Requires-Dist: google-generativeai>=0.8; extra == 'gemini'
|
|
50
53
|
Provides-Extra: openai
|
|
@@ -55,6 +58,24 @@ Requires-Dist: opentelemetry-exporter-otlp>=1.20; extra == 'otel'
|
|
|
55
58
|
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'otel'
|
|
56
59
|
Provides-Extra: process
|
|
57
60
|
Requires-Dist: psutil>=5.9; extra == 'process'
|
|
61
|
+
Provides-Extra: skills
|
|
62
|
+
Requires-Dist: caldav>=1.3; extra == 'skills'
|
|
63
|
+
Requires-Dist: croniter>=1.4; extra == 'skills'
|
|
64
|
+
Requires-Dist: duckduckgo-search>=7.0; extra == 'skills'
|
|
65
|
+
Requires-Dist: edge-tts>=7.0; extra == 'skills'
|
|
66
|
+
Requires-Dist: faster-whisper>=1.0; extra == 'skills'
|
|
67
|
+
Requires-Dist: feedparser>=6.0; extra == 'skills'
|
|
68
|
+
Requires-Dist: icalendar>=5.0; extra == 'skills'
|
|
69
|
+
Requires-Dist: markdown>=3.6; extra == 'skills'
|
|
70
|
+
Requires-Dist: openpyxl>=3.1; extra == 'skills'
|
|
71
|
+
Requires-Dist: pillow>=10.0; extra == 'skills'
|
|
72
|
+
Requires-Dist: psutil>=5.9; extra == 'skills'
|
|
73
|
+
Requires-Dist: pymupdf>=1.24; extra == 'skills'
|
|
74
|
+
Requires-Dist: pytesseract>=0.3; extra == 'skills'
|
|
75
|
+
Requires-Dist: python-docx>=1.1; extra == 'skills'
|
|
76
|
+
Requires-Dist: python-pptx>=1.0; extra == 'skills'
|
|
77
|
+
Requires-Dist: pyyaml>=6.0; extra == 'skills'
|
|
78
|
+
Requires-Dist: trafilatura>=2.0; extra == 'skills'
|
|
58
79
|
Provides-Extra: tokenizers
|
|
59
80
|
Requires-Dist: tiktoken>=0.7; extra == 'tokenizers'
|
|
60
81
|
Provides-Extra: vector
|
|
@@ -76,9 +97,6 @@ Description-Content-Type: text/markdown
|
|
|
76
97
|
<br/>
|
|
77
98
|
|
|
78
99
|
[](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
100
|
[](https://pepy.tech/project/echo-agent)
|
|
83
101
|
[](https://github.com/fuyuxiang/echo-agent)
|
|
84
102
|
|
|
@@ -202,9 +220,7 @@ PR 前请确保 lint 和测试通过(CI 会在 PR 上自动运行同样的检
|
|
|
202
220
|
**参与方向:** 通道适配器 · 内置工具 · MCP 集成 · 技能示例 · 评测数据集 · 文档完善 · 部署模板
|
|
203
221
|
|
|
204
222
|
**社区:**
|
|
205
|
-
- [
|
|
206
|
-
- [GitHub Issues](https://github.com/fuyuxiang/echo-agent/issues) — Bug 与新特性
|
|
207
|
-
- [QQ 群 47572014](https://qm.qq.com/q/JWOPDBNssw)
|
|
223
|
+
- QQ群:[47572014](https://qm.qq.com/q/JWOPDBNssw)
|
|
208
224
|
|
|
209
225
|
---
|
|
210
226
|
|
|
@@ -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,115 @@ 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
|
+
backend = getattr(ig, "backend", "openai") if ig else "openai"
|
|
193
|
+
|
|
194
|
+
if backend == "fal":
|
|
195
|
+
fal_key = getattr(ig, "fal_key", "") if ig else ""
|
|
196
|
+
fal_model = getattr(ig, "fal_model", "") if ig else ""
|
|
197
|
+
if not fal_key:
|
|
198
|
+
logger.info(
|
|
199
|
+
"image_generate tool not registered: no fal_key configured. "
|
|
200
|
+
"Set tools.image_gen.fal_key and tools.image_gen.fal_model in config to enable."
|
|
201
|
+
)
|
|
202
|
+
return
|
|
203
|
+
from echo_agent.agent.tools.image_gen_fal import FalImageGenTool, FAL_MODELS
|
|
204
|
+
if fal_model and fal_model not in FAL_MODELS:
|
|
205
|
+
logger.warning(
|
|
206
|
+
"image_generate: configured fal_model '{}' is not in the built-in catalog. "
|
|
207
|
+
"Supported: {}. The tool will error at execution time.",
|
|
208
|
+
fal_model, ", ".join(sorted(FAL_MODELS.keys())),
|
|
209
|
+
)
|
|
210
|
+
tools.append(FalImageGenTool(fal_key=fal_key, model=fal_model))
|
|
154
211
|
return
|
|
212
|
+
|
|
213
|
+
api_key = getattr(ig, "api_key", "") if ig else ""
|
|
214
|
+
api_base = getattr(ig, "api_base", "") if ig else ""
|
|
215
|
+
model = getattr(ig, "model", "") if ig else ""
|
|
216
|
+
|
|
217
|
+
if not api_key:
|
|
218
|
+
if _is_openai_compatible_provider(provider):
|
|
219
|
+
logger.info(
|
|
220
|
+
"image_generate tool not registered: no explicit image_gen.api_key configured. "
|
|
221
|
+
"Set tools.image_gen.api_key, tools.image_gen.api_base, and tools.image_gen.model in config to enable."
|
|
222
|
+
)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if not model:
|
|
226
|
+
model = _infer_image_model(api_base)
|
|
227
|
+
logger.debug("image_generate: model not configured, inferred '{}' from api_base", model)
|
|
228
|
+
|
|
155
229
|
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
|
-
))
|
|
230
|
+
tools.append(ImageGenTool(api_key=api_key, api_base=api_base, model=model))
|
|
161
231
|
|
|
162
232
|
|
|
163
|
-
def _try_register_tts(tools: list[Tool], config: Config, ws: str) -> None:
|
|
233
|
+
def _try_register_tts(tools: list[Tool], config: Config, ws: str, provider: LLMProvider | None = None) -> None:
|
|
164
234
|
from echo_agent.agent.tools.tts import TTSTool
|
|
165
235
|
tts_cfg = getattr(config.tools, "tts", None)
|
|
166
236
|
openai_key = getattr(tts_cfg, "openai_api_key", "") if tts_cfg else ""
|
|
167
|
-
|
|
237
|
+
openai_base = getattr(tts_cfg, "openai_api_base", "") if tts_cfg else ""
|
|
238
|
+
tts_model = getattr(tts_cfg, "model", "") if tts_cfg else ""
|
|
239
|
+
default_backend = getattr(tts_cfg, "default_backend", "edge") if tts_cfg else "edge"
|
|
240
|
+
default_voice = getattr(tts_cfg, "default_voice", "") if tts_cfg else ""
|
|
241
|
+
|
|
242
|
+
if not openai_key and _is_openai_compatible_provider(provider):
|
|
243
|
+
logger.info(
|
|
244
|
+
"TTS openai backend not available: no explicit tts.openai_api_key configured. "
|
|
245
|
+
"Set tools.tts.openai_api_key, tools.tts.openai_api_base, and tools.tts.model in config to enable. "
|
|
246
|
+
"edge backend remains available without configuration."
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if not tts_model and openai_key:
|
|
250
|
+
tts_model = _infer_tts_model(openai_base)
|
|
251
|
+
logger.debug("TTS: model not configured, inferred '{}' from openai_api_base", tts_model)
|
|
252
|
+
if not tts_model:
|
|
253
|
+
tts_model = "tts-1"
|
|
254
|
+
|
|
255
|
+
tools.append(TTSTool(
|
|
256
|
+
workspace=ws,
|
|
257
|
+
openai_api_key=openai_key,
|
|
258
|
+
openai_api_base=openai_base,
|
|
259
|
+
tts_model=tts_model,
|
|
260
|
+
default_backend=default_backend,
|
|
261
|
+
default_voice=default_voice,
|
|
262
|
+
))
|