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.
Files changed (294) hide show
  1. {echo_agent-0.2.0 → echo_agent-0.2.1}/PKG-INFO +20 -7
  2. {echo_agent-0.2.0 → echo_agent-0.2.1}/README.md +1 -6
  3. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/compressor.py +8 -0
  4. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/context.py +149 -10
  5. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/pipeline/context_stage.py +48 -11
  6. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/__init__.py +89 -11
  7. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/tts.py +9 -5
  8. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/base.py +42 -1
  9. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/feishu.py +23 -1
  10. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/matrix.py +29 -2
  11. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/slack.py +24 -4
  12. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/telegram.py +28 -2
  13. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/weixin.py +21 -6
  14. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/whatsapp.py +29 -3
  15. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/config/schema.py +4 -0
  16. echo_agent-0.2.1/echo_agent/dependencies/__init__.py +37 -0
  17. echo_agent-0.2.1/echo_agent/dependencies/cli.py +167 -0
  18. echo_agent-0.2.1/echo_agent/dependencies/lazy_deps.py +488 -0
  19. echo_agent-0.2.1/echo_agent/dependencies/skill_require.py +133 -0
  20. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/consolidator.py +14 -1
  21. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/bedrock_provider.py +28 -2
  22. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/format_utils.py +33 -3
  23. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/gemini_provider.py +24 -2
  24. echo_agent-0.2.1/echo_agent/session/media_ref.py +42 -0
  25. {echo_agent-0.2.0 → echo_agent-0.2.1}/pyproject.toml +20 -1
  26. echo_agent-0.2.1/skills/creative/excel-author/SKILL.md +88 -0
  27. echo_agent-0.2.1/skills/creative/excel-author/scripts/create_xlsx.py +97 -0
  28. echo_agent-0.2.1/skills/creative/image-gen/SKILL.md +74 -0
  29. echo_agent-0.2.1/skills/creative/image-gen/scripts/generate_image.py +73 -0
  30. echo_agent-0.2.1/skills/creative/meme-gen/SKILL.md +66 -0
  31. echo_agent-0.2.1/skills/creative/meme-gen/scripts/make_meme.py +121 -0
  32. echo_agent-0.2.1/skills/creative/ppt-author/SKILL.md +87 -0
  33. echo_agent-0.2.1/skills/creative/ppt-author/scripts/create_pptx.py +89 -0
  34. echo_agent-0.2.1/skills/development/code-runner/SKILL.md +71 -0
  35. echo_agent-0.2.1/skills/development/code-runner/scripts/safe_exec.py +133 -0
  36. echo_agent-0.2.1/skills/development/github-ops/SKILL.md +81 -0
  37. {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/development/skill-creator/scripts/init_skill.py +0 -0
  38. {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/development/skill-creator/scripts/package_skill.py +0 -0
  39. {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/development/skill-creator/scripts/quick_validate.py +0 -0
  40. echo_agent-0.2.1/skills/development/workflow-chain/SKILL.md +103 -0
  41. echo_agent-0.2.1/skills/development/workflow-chain/scripts/workflow_engine.py +100 -0
  42. echo_agent-0.2.1/skills/devops/docker-manage/SKILL.md +85 -0
  43. echo_agent-0.2.1/skills/devops/system-monitor/SKILL.md +77 -0
  44. echo_agent-0.2.1/skills/devops/system-monitor/scripts/system_check.py +110 -0
  45. echo_agent-0.2.1/skills/finance/finance-tracker/SKILL.md +66 -0
  46. echo_agent-0.2.1/skills/finance/finance-tracker/scripts/finance_manager.py +141 -0
  47. echo_agent-0.2.1/skills/finance/stocks/SKILL.md +75 -0
  48. echo_agent-0.2.1/skills/finance/stocks/scripts/market_query.py +99 -0
  49. echo_agent-0.2.1/skills/health/fitness-nutrition/SKILL.md +68 -0
  50. echo_agent-0.2.1/skills/health/fitness-nutrition/scripts/health_query.py +83 -0
  51. echo_agent-0.2.1/skills/learning/flashcards/SKILL.md +80 -0
  52. echo_agent-0.2.1/skills/learning/flashcards/scripts/flashcard_engine.py +170 -0
  53. echo_agent-0.2.1/skills/media/tts-voice/SKILL.md +82 -0
  54. echo_agent-0.2.1/skills/media/tts-voice/scripts/text_to_speech.py +55 -0
  55. echo_agent-0.2.1/skills/media/voice-note/SKILL.md +87 -0
  56. echo_agent-0.2.1/skills/media/voice-note/scripts/voice_process.py +68 -0
  57. echo_agent-0.2.1/skills/productivity/calendar/SKILL.md +64 -0
  58. echo_agent-0.2.1/skills/productivity/calendar/scripts/calendar_client.py +96 -0
  59. echo_agent-0.2.1/skills/productivity/daily-briefing/SKILL.md +69 -0
  60. echo_agent-0.2.1/skills/productivity/daily-briefing/scripts/generate_briefing.py +57 -0
  61. echo_agent-0.2.1/skills/productivity/email-assistant/SKILL.md +80 -0
  62. echo_agent-0.2.1/skills/productivity/email-assistant/scripts/email_client.py +99 -0
  63. echo_agent-0.2.1/skills/productivity/note-taking/SKILL.md +60 -0
  64. echo_agent-0.2.1/skills/productivity/note-taking/scripts/notes_manager.py +126 -0
  65. echo_agent-0.2.1/skills/productivity/notion-sync/SKILL.md +80 -0
  66. echo_agent-0.2.1/skills/productivity/notion-sync/scripts/notion_client.py +81 -0
  67. echo_agent-0.2.1/skills/productivity/ocr-document/SKILL.md +86 -0
  68. echo_agent-0.2.1/skills/productivity/ocr-document/scripts/extract_document.py +75 -0
  69. echo_agent-0.2.1/skills/productivity/reminder/SKILL.md +65 -0
  70. echo_agent-0.2.1/skills/productivity/reminder/scripts/reminder_store.py +112 -0
  71. {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/research/arxiv/scripts/search_arxiv.py +0 -0
  72. echo_agent-0.2.1/skills/research/deep-research/SKILL.md +74 -0
  73. echo_agent-0.2.1/skills/research/deep-research/scripts/research_report.py +95 -0
  74. echo_agent-0.2.1/skills/research/rss-watcher/SKILL.md +70 -0
  75. echo_agent-0.2.1/skills/research/rss-watcher/scripts/feed_monitor.py +100 -0
  76. echo_agent-0.2.1/skills/research/web-extract/SKILL.md +82 -0
  77. echo_agent-0.2.1/skills/research/web-extract/scripts/extract_url.py +38 -0
  78. echo_agent-0.2.1/skills/research/web-search/SKILL.md +80 -0
  79. echo_agent-0.2.1/skills/research/web-search/scripts/web_search.py +54 -0
  80. echo_agent-0.2.1/skills/utility/calculator/SKILL.md +91 -0
  81. echo_agent-0.2.1/skills/utility/calculator/scripts/calc.py +103 -0
  82. echo_agent-0.2.1/skills/utility/file-convert/SKILL.md +59 -0
  83. echo_agent-0.2.1/skills/utility/file-convert/scripts/convert.py +83 -0
  84. echo_agent-0.2.1/skills/utility/maps-poi/SKILL.md +77 -0
  85. echo_agent-0.2.1/skills/utility/maps-poi/scripts/geo_query.py +86 -0
  86. echo_agent-0.2.1/skills/utility/text-tools/SKILL.md +86 -0
  87. echo_agent-0.2.1/skills/utility/text-tools/scripts/text_process.py +94 -0
  88. {echo_agent-0.2.0 → echo_agent-0.2.1}/.gitignore +0 -0
  89. {echo_agent-0.2.0 → echo_agent-0.2.1}/LICENSE +0 -0
  90. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/__init__.py +0 -0
  91. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/__main__.py +0 -0
  92. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/a2a/__init__.py +0 -0
  93. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/a2a/client.py +0 -0
  94. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/a2a/models.py +0 -0
  95. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/a2a/protocol.py +0 -0
  96. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/a2a/server.py +0 -0
  97. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/__init__.py +0 -0
  98. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/approval_gate.py +0 -0
  99. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/__init__.py +0 -0
  100. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/assembler.py +0 -0
  101. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/boundary.py +0 -0
  102. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/engine.py +0 -0
  103. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/pruner.py +0 -0
  104. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/summarizer.py +0 -0
  105. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/types.py +0 -0
  106. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/compression/validator.py +0 -0
  107. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/consolidation.py +0 -0
  108. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/context_cache.py +0 -0
  109. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/executors/__init__.py +0 -0
  110. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/executors/base.py +0 -0
  111. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/executors/factory.py +0 -0
  112. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/executors/remote.py +0 -0
  113. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/loop.py +0 -0
  114. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/__init__.py +0 -0
  115. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/audit.py +0 -0
  116. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/error_messages.py +0 -0
  117. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/error_types.py +0 -0
  118. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/models.py +0 -0
  119. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/registry.py +0 -0
  120. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/multi_agent/runtime.py +0 -0
  121. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/pipeline/__init__.py +0 -0
  122. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/pipeline/inference_stage.py +0 -0
  123. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/pipeline/response_stage.py +0 -0
  124. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/pipeline/types.py +0 -0
  125. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/planning/__init__.py +0 -0
  126. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/planning/models.py +0 -0
  127. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/planning/planner.py +0 -0
  128. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/planning/reflection.py +0 -0
  129. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/planning/strategies.py +0 -0
  130. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/streaming.py +0 -0
  131. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/base.py +0 -0
  132. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/circuit_breaker.py +0 -0
  133. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/clarify.py +0 -0
  134. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/code_exec.py +0 -0
  135. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/cronjob.py +0 -0
  136. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/delegate.py +0 -0
  137. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/filesystem.py +0 -0
  138. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/image_gen.py +0 -0
  139. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/knowledge.py +0 -0
  140. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/memory.py +0 -0
  141. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/message.py +0 -0
  142. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/notify.py +0 -0
  143. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/patch.py +0 -0
  144. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/process.py +0 -0
  145. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/registry.py +0 -0
  146. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/search.py +0 -0
  147. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/session_search.py +0 -0
  148. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/shell.py +0 -0
  149. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/skill_install.py +0 -0
  150. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/skills.py +0 -0
  151. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/task.py +0 -0
  152. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/todo.py +0 -0
  153. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/vision.py +0 -0
  154. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/web.py +0 -0
  155. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/agent/tools/workflow.py +0 -0
  156. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/app.py +0 -0
  157. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/bus/__init__.py +0 -0
  158. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/bus/events.py +0 -0
  159. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/bus/queue.py +0 -0
  160. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/bus/rate_limiter.py +0 -0
  161. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/__init__.py +0 -0
  162. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/cli.py +0 -0
  163. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/cron.py +0 -0
  164. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/dingtalk.py +0 -0
  165. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/discord.py +0 -0
  166. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/email.py +0 -0
  167. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/manager.py +0 -0
  168. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/qqbot.py +0 -0
  169. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/qqbot_media.py +0 -0
  170. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/webhook.py +0 -0
  171. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/channels/wecom.py +0 -0
  172. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/__init__.py +0 -0
  173. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/colors.py +0 -0
  174. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/evolution_cmd.py +0 -0
  175. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/i18n/__init__.py +0 -0
  176. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/i18n/en.py +0 -0
  177. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/i18n/zh.py +0 -0
  178. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/plugins_cmd.py +0 -0
  179. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/prompt.py +0 -0
  180. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/service.py +0 -0
  181. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/setup.py +0 -0
  182. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/cli/status.py +0 -0
  183. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/config/__init__.py +0 -0
  184. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/config/default.yaml +0 -0
  185. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/config/loader.py +0 -0
  186. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evaluation/__init__.py +0 -0
  187. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evaluation/dataset.py +0 -0
  188. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evaluation/metrics.py +0 -0
  189. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evaluation/reporter.py +0 -0
  190. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evaluation/runner.py +0 -0
  191. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/__init__.py +0 -0
  192. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/engine.py +0 -0
  193. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/evolver.py +0 -0
  194. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/gate.py +0 -0
  195. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/recorder.py +0 -0
  196. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/scheduler.py +0 -0
  197. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/store.py +0 -0
  198. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/tools.py +0 -0
  199. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/types.py +0 -0
  200. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/evolution/validation.py +0 -0
  201. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/__init__.py +0 -0
  202. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/auth.py +0 -0
  203. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/editor.py +0 -0
  204. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/health.py +0 -0
  205. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/hooks.py +0 -0
  206. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/media.py +0 -0
  207. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/rate_limiter.py +0 -0
  208. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/router.py +0 -0
  209. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/server.py +0 -0
  210. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/session_context.py +0 -0
  211. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/session_policy.py +0 -0
  212. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/gateway/static/index.html +0 -0
  213. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/knowledge/__init__.py +0 -0
  214. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/knowledge/index.py +0 -0
  215. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/__init__.py +0 -0
  216. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/client.py +0 -0
  217. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/manager.py +0 -0
  218. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/oauth.py +0 -0
  219. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/security.py +0 -0
  220. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/tool_adapter.py +0 -0
  221. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/mcp/transport.py +0 -0
  222. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/__init__.py +0 -0
  223. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/contradiction.py +0 -0
  224. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/forgetting.py +0 -0
  225. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/retrieval.py +0 -0
  226. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/reviewer.py +0 -0
  227. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/store.py +0 -0
  228. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/tiers.py +0 -0
  229. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/types.py +0 -0
  230. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/memory/vectors.py +0 -0
  231. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/__init__.py +0 -0
  232. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/credential_pool.py +0 -0
  233. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/inference.py +0 -0
  234. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/provider.py +0 -0
  235. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/__init__.py +0 -0
  236. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/anthropic_provider.py +0 -0
  237. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/openai_provider.py +0 -0
  238. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/providers/openrouter_provider.py +0 -0
  239. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/rate_limiter.py +0 -0
  240. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/router.py +0 -0
  241. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/stub.py +0 -0
  242. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/models/tokenizer.py +0 -0
  243. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/observability/__init__.py +0 -0
  244. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/observability/monitor.py +0 -0
  245. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/observability/spans.py +0 -0
  246. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/observability/telemetry.py +0 -0
  247. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/permissions/__init__.py +0 -0
  248. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/permissions/allowlist.py +0 -0
  249. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/permissions/manager.py +0 -0
  250. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/__init__.py +0 -0
  251. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/context.py +0 -0
  252. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/errors.py +0 -0
  253. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/hooks.py +0 -0
  254. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/loader.py +0 -0
  255. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/manager.py +0 -0
  256. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/manifest.py +0 -0
  257. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/plugins/sandbox.py +0 -0
  258. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/runtime_paths.py +0 -0
  259. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/scheduler/__init__.py +0 -0
  260. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/scheduler/delivery.py +0 -0
  261. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/scheduler/service.py +0 -0
  262. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/__init__.py +0 -0
  263. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/capabilities.py +0 -0
  264. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/guards.py +0 -0
  265. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/normalizer.py +0 -0
  266. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/path_policy.py +0 -0
  267. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/risk_classifier.py +0 -0
  268. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/smart_approval.py +0 -0
  269. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/tokenizer.py +0 -0
  270. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/security/tool_policy.py +0 -0
  271. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/session/__init__.py +0 -0
  272. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/session/manager.py +0 -0
  273. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/skills/__init__.py +0 -0
  274. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/skills/manager.py +0 -0
  275. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/skills/reviewer.py +0 -0
  276. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/skills/store.py +0 -0
  277. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/storage/__init__.py +0 -0
  278. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/storage/backend.py +0 -0
  279. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/storage/sqlite.py +0 -0
  280. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tasks/__init__.py +0 -0
  281. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tasks/manager.py +0 -0
  282. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tasks/models.py +0 -0
  283. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tasks/workflow.py +0 -0
  284. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tools/__init__.py +0 -0
  285. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/tools/base.py +0 -0
  286. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/utils/__init__.py +0 -0
  287. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/utils/async_io.py +0 -0
  288. {echo_agent-0.2.0 → echo_agent-0.2.1}/echo_agent/utils/text.py +0 -0
  289. {echo_agent-0.2.0 → echo_agent-0.2.1}/scripts/install.sh +0 -0
  290. {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/development/plan/SKILL.md +0 -0
  291. {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/development/skill-creator/SKILL.md +0 -0
  292. {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/productivity/summarize/SKILL.md +0 -0
  293. {echo_agent-0.2.0 → echo_agent-0.2.1}/skills/productivity/weather/SKILL.md +0 -0
  294. {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.0
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
  [![PyPI](https://img.shields.io/pypi/v/echo-agent)](https://pypi.org/project/echo-agent/)
79
- [![Python](https://img.shields.io/pypi/pyversions/echo-agent)](https://pypi.org/project/echo-agent/)
80
- [![CI](https://github.com/fuyuxiang/echo-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/fuyuxiang/echo-agent/actions/workflows/ci.yml)
81
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
82
97
  [![Downloads](https://static.pepy.tech/badge/echo-agent)](https://pepy.tech/project/echo-agent)
83
98
  [![GitHub stars](https://img.shields.io/github/stars/fuyuxiang/echo-agent?style=social)](https://github.com/fuyuxiang/echo-agent)
84
99
 
@@ -202,9 +217,7 @@ PR 前请确保 lint 和测试通过(CI 会在 PR 上自动运行同样的检
202
217
  **参与方向:** 通道适配器 · 内置工具 · MCP 集成 · 技能示例 · 评测数据集 · 文档完善 · 部署模板
203
218
 
204
219
  **社区:**
205
- - [GitHub Discussions](https://github.com/fuyuxiang/echo-agent/discussions) — 设计讨论、用法咨询
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
  [![PyPI](https://img.shields.io/pypi/v/echo-agent)](https://pypi.org/project/echo-agent/)
14
- [![Python](https://img.shields.io/pypi/pyversions/echo-agent)](https://pypi.org/project/echo-agent/)
15
- [![CI](https://github.com/fuyuxiang/echo-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/fuyuxiang/echo-agent/actions/workflows/ci.yml)
16
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
17
14
  [![Downloads](https://static.pepy.tech/badge/echo-agent)](https://pepy.tech/project/echo-agent)
18
15
  [![GitHub stars](https://img.shields.io/github/stars/fuyuxiang/echo-agent?style=social)](https://github.com/fuyuxiang/echo-agent)
19
16
 
@@ -137,9 +134,7 @@ PR 前请确保 lint 和测试通过(CI 会在 PR 上自动运行同样的检
137
134
  **参与方向:** 通道适配器 · 内置工具 · MCP 集成 · 技能示例 · 评测数据集 · 文档完善 · 部署模板
138
135
 
139
136
  **社区:**
140
- - [GitHub Discussions](https://github.com/fuyuxiang/echo-agent/discussions) — 设计讨论、用法咨询
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
- session.add_message("user", event.text)
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
- media_items = event.media_items
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
- messages[-1]["content"] + f"\n\n[Plan]\n{plan_context}"
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 _try_register_image_gen(tools: list[Tool], config: Config) -> None:
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
- if not ig or not getattr(ig, "api_key", ""):
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
- tools.append(TTSTool(workspace=ws, openai_api_key=openai_key))
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", "edge" if not self._openai_key else "openai")
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 = "https://api.openai.com/v1/audio/speech"
66
+ url = f"{self._openai_base}/audio/speech"
63
67
  headers = {"Authorization": f"Bearer {self._openai_key}", "Content-Type": "application/json"}
64
- body = {"model": "tts-1", "input": text, "voice": voice, "response_format": "mp3"}
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={"name": name} if name else {},
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
- media.append({"type": "image", "url": image_key})
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: