echo-agent 0.2.1__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.
Files changed (301) hide show
  1. {echo_agent-0.2.1 → echo_agent-0.2.2}/PKG-INFO +4 -1
  2. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/__init__.py +42 -25
  3. echo_agent-0.2.2/echo_agent/agent/tools/image_gen_fal.py +231 -0
  4. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/base.py +15 -0
  5. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/dingtalk.py +2 -0
  6. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/email.py +2 -0
  7. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/feishu.py +2 -0
  8. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/matrix.py +2 -0
  9. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/qqbot.py +2 -0
  10. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/webhook.py +2 -0
  11. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/wecom.py +2 -0
  12. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/weixin.py +2 -0
  13. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/whatsapp.py +2 -0
  14. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/i18n/en.py +8 -0
  15. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/i18n/zh.py +8 -0
  16. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/setup.py +32 -10
  17. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/config/schema.py +8 -1
  18. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/dependencies/lazy_deps.py +1 -0
  19. echo_agent-0.2.2/echo_agent/gateway/api/__init__.py +50 -0
  20. echo_agent-0.2.2/echo_agent/gateway/api/channels.py +53 -0
  21. echo_agent-0.2.2/echo_agent/gateway/api/config.py +85 -0
  22. echo_agent-0.2.2/echo_agent/gateway/api/knowledge.py +136 -0
  23. echo_agent-0.2.2/echo_agent/gateway/api/memory.py +137 -0
  24. echo_agent-0.2.2/echo_agent/gateway/api/skills.py +66 -0
  25. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/server.py +4 -0
  26. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/security/capabilities.py +2 -2
  27. {echo_agent-0.2.1 → echo_agent-0.2.2}/pyproject.toml +3 -2
  28. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/creative/ppt-author/scripts/create_pptx.py +15 -10
  29. {echo_agent-0.2.1 → echo_agent-0.2.2}/.gitignore +0 -0
  30. {echo_agent-0.2.1 → echo_agent-0.2.2}/LICENSE +0 -0
  31. {echo_agent-0.2.1 → echo_agent-0.2.2}/README.md +0 -0
  32. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/__init__.py +0 -0
  33. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/__main__.py +0 -0
  34. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/a2a/__init__.py +0 -0
  35. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/a2a/client.py +0 -0
  36. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/a2a/models.py +0 -0
  37. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/a2a/protocol.py +0 -0
  38. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/a2a/server.py +0 -0
  39. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/__init__.py +0 -0
  40. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/approval_gate.py +0 -0
  41. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/compression/__init__.py +0 -0
  42. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/compression/assembler.py +0 -0
  43. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/compression/boundary.py +0 -0
  44. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/compression/compressor.py +0 -0
  45. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/compression/engine.py +0 -0
  46. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/compression/pruner.py +0 -0
  47. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/compression/summarizer.py +0 -0
  48. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/compression/types.py +0 -0
  49. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/compression/validator.py +0 -0
  50. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/consolidation.py +0 -0
  51. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/context.py +0 -0
  52. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/context_cache.py +0 -0
  53. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/executors/__init__.py +0 -0
  54. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/executors/base.py +0 -0
  55. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/executors/factory.py +0 -0
  56. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/executors/remote.py +0 -0
  57. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/loop.py +0 -0
  58. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/__init__.py +0 -0
  59. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/audit.py +0 -0
  60. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/error_messages.py +0 -0
  61. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/error_types.py +0 -0
  62. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/models.py +0 -0
  63. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/registry.py +0 -0
  64. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/multi_agent/runtime.py +0 -0
  65. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/pipeline/__init__.py +0 -0
  66. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/pipeline/context_stage.py +0 -0
  67. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/pipeline/inference_stage.py +0 -0
  68. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/pipeline/response_stage.py +0 -0
  69. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/pipeline/types.py +0 -0
  70. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/planning/__init__.py +0 -0
  71. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/planning/models.py +0 -0
  72. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/planning/planner.py +0 -0
  73. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/planning/reflection.py +0 -0
  74. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/planning/strategies.py +0 -0
  75. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/streaming.py +0 -0
  76. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/base.py +0 -0
  77. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/circuit_breaker.py +0 -0
  78. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/clarify.py +0 -0
  79. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/code_exec.py +0 -0
  80. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/cronjob.py +0 -0
  81. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/delegate.py +0 -0
  82. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/filesystem.py +0 -0
  83. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/image_gen.py +0 -0
  84. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/knowledge.py +0 -0
  85. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/memory.py +0 -0
  86. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/message.py +0 -0
  87. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/notify.py +0 -0
  88. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/patch.py +0 -0
  89. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/process.py +0 -0
  90. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/registry.py +0 -0
  91. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/search.py +0 -0
  92. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/session_search.py +0 -0
  93. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/shell.py +0 -0
  94. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/skill_install.py +0 -0
  95. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/skills.py +0 -0
  96. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/task.py +0 -0
  97. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/todo.py +0 -0
  98. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/tts.py +0 -0
  99. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/vision.py +0 -0
  100. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/web.py +0 -0
  101. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/agent/tools/workflow.py +0 -0
  102. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/app.py +0 -0
  103. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/bus/__init__.py +0 -0
  104. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/bus/events.py +0 -0
  105. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/bus/queue.py +0 -0
  106. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/bus/rate_limiter.py +0 -0
  107. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/__init__.py +0 -0
  108. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/cli.py +0 -0
  109. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/cron.py +0 -0
  110. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/discord.py +0 -0
  111. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/manager.py +0 -0
  112. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/qqbot_media.py +0 -0
  113. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/slack.py +0 -0
  114. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/channels/telegram.py +0 -0
  115. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/__init__.py +0 -0
  116. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/colors.py +0 -0
  117. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/evolution_cmd.py +0 -0
  118. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/i18n/__init__.py +0 -0
  119. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/plugins_cmd.py +0 -0
  120. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/prompt.py +0 -0
  121. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/service.py +0 -0
  122. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/cli/status.py +0 -0
  123. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/config/__init__.py +0 -0
  124. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/config/default.yaml +0 -0
  125. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/config/loader.py +0 -0
  126. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/dependencies/__init__.py +0 -0
  127. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/dependencies/cli.py +0 -0
  128. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/dependencies/skill_require.py +0 -0
  129. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evaluation/__init__.py +0 -0
  130. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evaluation/dataset.py +0 -0
  131. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evaluation/metrics.py +0 -0
  132. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evaluation/reporter.py +0 -0
  133. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evaluation/runner.py +0 -0
  134. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evolution/__init__.py +0 -0
  135. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evolution/engine.py +0 -0
  136. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evolution/evolver.py +0 -0
  137. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evolution/gate.py +0 -0
  138. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evolution/recorder.py +0 -0
  139. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evolution/scheduler.py +0 -0
  140. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evolution/store.py +0 -0
  141. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evolution/tools.py +0 -0
  142. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evolution/types.py +0 -0
  143. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/evolution/validation.py +0 -0
  144. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/__init__.py +0 -0
  145. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/auth.py +0 -0
  146. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/editor.py +0 -0
  147. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/health.py +0 -0
  148. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/hooks.py +0 -0
  149. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/media.py +0 -0
  150. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/rate_limiter.py +0 -0
  151. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/router.py +0 -0
  152. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/session_context.py +0 -0
  153. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/session_policy.py +0 -0
  154. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/gateway/static/index.html +0 -0
  155. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/knowledge/__init__.py +0 -0
  156. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/knowledge/index.py +0 -0
  157. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/mcp/__init__.py +0 -0
  158. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/mcp/client.py +0 -0
  159. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/mcp/manager.py +0 -0
  160. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/mcp/oauth.py +0 -0
  161. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/mcp/security.py +0 -0
  162. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/mcp/tool_adapter.py +0 -0
  163. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/mcp/transport.py +0 -0
  164. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/memory/__init__.py +0 -0
  165. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/memory/consolidator.py +0 -0
  166. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/memory/contradiction.py +0 -0
  167. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/memory/forgetting.py +0 -0
  168. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/memory/retrieval.py +0 -0
  169. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/memory/reviewer.py +0 -0
  170. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/memory/store.py +0 -0
  171. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/memory/tiers.py +0 -0
  172. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/memory/types.py +0 -0
  173. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/memory/vectors.py +0 -0
  174. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/__init__.py +0 -0
  175. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/credential_pool.py +0 -0
  176. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/inference.py +0 -0
  177. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/provider.py +0 -0
  178. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/providers/__init__.py +0 -0
  179. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/providers/anthropic_provider.py +0 -0
  180. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/providers/bedrock_provider.py +0 -0
  181. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/providers/format_utils.py +0 -0
  182. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/providers/gemini_provider.py +0 -0
  183. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/providers/openai_provider.py +0 -0
  184. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/providers/openrouter_provider.py +0 -0
  185. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/rate_limiter.py +0 -0
  186. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/router.py +0 -0
  187. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/stub.py +0 -0
  188. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/models/tokenizer.py +0 -0
  189. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/observability/__init__.py +0 -0
  190. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/observability/monitor.py +0 -0
  191. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/observability/spans.py +0 -0
  192. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/observability/telemetry.py +0 -0
  193. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/permissions/__init__.py +0 -0
  194. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/permissions/allowlist.py +0 -0
  195. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/permissions/manager.py +0 -0
  196. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/plugins/__init__.py +0 -0
  197. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/plugins/context.py +0 -0
  198. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/plugins/errors.py +0 -0
  199. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/plugins/hooks.py +0 -0
  200. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/plugins/loader.py +0 -0
  201. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/plugins/manager.py +0 -0
  202. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/plugins/manifest.py +0 -0
  203. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/plugins/sandbox.py +0 -0
  204. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/runtime_paths.py +0 -0
  205. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/scheduler/__init__.py +0 -0
  206. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/scheduler/delivery.py +0 -0
  207. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/scheduler/service.py +0 -0
  208. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/security/__init__.py +0 -0
  209. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/security/guards.py +0 -0
  210. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/security/normalizer.py +0 -0
  211. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/security/path_policy.py +0 -0
  212. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/security/risk_classifier.py +0 -0
  213. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/security/smart_approval.py +0 -0
  214. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/security/tokenizer.py +0 -0
  215. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/security/tool_policy.py +0 -0
  216. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/session/__init__.py +0 -0
  217. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/session/manager.py +0 -0
  218. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/session/media_ref.py +0 -0
  219. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/skills/__init__.py +0 -0
  220. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/skills/manager.py +0 -0
  221. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/skills/reviewer.py +0 -0
  222. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/skills/store.py +0 -0
  223. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/storage/__init__.py +0 -0
  224. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/storage/backend.py +0 -0
  225. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/storage/sqlite.py +0 -0
  226. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/tasks/__init__.py +0 -0
  227. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/tasks/manager.py +0 -0
  228. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/tasks/models.py +0 -0
  229. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/tasks/workflow.py +0 -0
  230. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/tools/__init__.py +0 -0
  231. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/tools/base.py +0 -0
  232. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/utils/__init__.py +0 -0
  233. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/utils/async_io.py +0 -0
  234. {echo_agent-0.2.1 → echo_agent-0.2.2}/echo_agent/utils/text.py +0 -0
  235. {echo_agent-0.2.1 → echo_agent-0.2.2}/scripts/install.sh +0 -0
  236. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/creative/excel-author/SKILL.md +0 -0
  237. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/creative/excel-author/scripts/create_xlsx.py +0 -0
  238. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/creative/image-gen/SKILL.md +0 -0
  239. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/creative/image-gen/scripts/generate_image.py +0 -0
  240. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/creative/meme-gen/SKILL.md +0 -0
  241. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/creative/meme-gen/scripts/make_meme.py +0 -0
  242. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/creative/ppt-author/SKILL.md +0 -0
  243. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/development/code-runner/SKILL.md +0 -0
  244. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/development/code-runner/scripts/safe_exec.py +0 -0
  245. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/development/github-ops/SKILL.md +0 -0
  246. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/development/plan/SKILL.md +0 -0
  247. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/development/skill-creator/SKILL.md +0 -0
  248. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/development/skill-creator/scripts/init_skill.py +0 -0
  249. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/development/skill-creator/scripts/package_skill.py +0 -0
  250. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/development/skill-creator/scripts/quick_validate.py +0 -0
  251. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/development/workflow-chain/SKILL.md +0 -0
  252. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/development/workflow-chain/scripts/workflow_engine.py +0 -0
  253. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/devops/docker-manage/SKILL.md +0 -0
  254. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/devops/system-monitor/SKILL.md +0 -0
  255. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/devops/system-monitor/scripts/system_check.py +0 -0
  256. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/finance/finance-tracker/SKILL.md +0 -0
  257. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/finance/finance-tracker/scripts/finance_manager.py +0 -0
  258. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/finance/stocks/SKILL.md +0 -0
  259. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/finance/stocks/scripts/market_query.py +0 -0
  260. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/health/fitness-nutrition/SKILL.md +0 -0
  261. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/health/fitness-nutrition/scripts/health_query.py +0 -0
  262. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/learning/flashcards/SKILL.md +0 -0
  263. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/learning/flashcards/scripts/flashcard_engine.py +0 -0
  264. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/media/tts-voice/SKILL.md +0 -0
  265. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/media/tts-voice/scripts/text_to_speech.py +0 -0
  266. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/media/voice-note/SKILL.md +0 -0
  267. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/media/voice-note/scripts/voice_process.py +0 -0
  268. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/calendar/SKILL.md +0 -0
  269. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/calendar/scripts/calendar_client.py +0 -0
  270. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/daily-briefing/SKILL.md +0 -0
  271. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/daily-briefing/scripts/generate_briefing.py +0 -0
  272. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/email-assistant/SKILL.md +0 -0
  273. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/email-assistant/scripts/email_client.py +0 -0
  274. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/note-taking/SKILL.md +0 -0
  275. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/note-taking/scripts/notes_manager.py +0 -0
  276. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/notion-sync/SKILL.md +0 -0
  277. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/notion-sync/scripts/notion_client.py +0 -0
  278. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/ocr-document/SKILL.md +0 -0
  279. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/ocr-document/scripts/extract_document.py +0 -0
  280. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/reminder/SKILL.md +0 -0
  281. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/reminder/scripts/reminder_store.py +0 -0
  282. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/summarize/SKILL.md +0 -0
  283. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/productivity/weather/SKILL.md +0 -0
  284. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/research/arxiv/SKILL.md +0 -0
  285. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/research/arxiv/scripts/search_arxiv.py +0 -0
  286. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/research/deep-research/SKILL.md +0 -0
  287. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/research/deep-research/scripts/research_report.py +0 -0
  288. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/research/rss-watcher/SKILL.md +0 -0
  289. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/research/rss-watcher/scripts/feed_monitor.py +0 -0
  290. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/research/web-extract/SKILL.md +0 -0
  291. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/research/web-extract/scripts/extract_url.py +0 -0
  292. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/research/web-search/SKILL.md +0 -0
  293. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/research/web-search/scripts/web_search.py +0 -0
  294. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/utility/calculator/SKILL.md +0 -0
  295. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/utility/calculator/scripts/calc.py +0 -0
  296. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/utility/file-convert/SKILL.md +0 -0
  297. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/utility/file-convert/scripts/convert.py +0 -0
  298. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/utility/maps-poi/SKILL.md +0 -0
  299. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/utility/maps-poi/scripts/geo_query.py +0 -0
  300. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/utility/text-tools/SKILL.md +0 -0
  301. {echo_agent-0.2.1 → echo_agent-0.2.2}/skills/utility/text-tools/scripts/text_process.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echo-agent
3
- Version: 0.2.1
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
@@ -189,30 +189,42 @@ def _infer_tts_model(api_base: str) -> str:
189
189
 
190
190
  def _try_register_image_gen(tools: list[Tool], config: Config, provider: LLMProvider | None = None) -> None:
191
191
  ig = getattr(config.tools, "image_gen", None)
192
- explicit_key = getattr(ig, "api_key", "") if ig else ""
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))
211
+ return
212
+
213
+ api_key = getattr(ig, "api_key", "") if ig else ""
193
214
  api_base = getattr(ig, "api_base", "") if ig else ""
194
215
  model = getattr(ig, "model", "") if ig else ""
195
216
 
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
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
+ )
211
223
  return
212
224
 
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"):
225
+ if not model:
215
226
  model = _infer_image_model(api_base)
227
+ logger.debug("image_generate: model not configured, inferred '{}' from api_base", model)
216
228
 
217
229
  from echo_agent.agent.tools.image_gen import ImageGenTool
218
230
  tools.append(ImageGenTool(api_key=api_key, api_base=api_base, model=model))
@@ -222,18 +234,23 @@ def _try_register_tts(tools: list[Tool], config: Config, ws: str, provider: LLMP
222
234
  from echo_agent.agent.tools.tts import TTSTool
223
235
  tts_cfg = getattr(config.tools, "tts", None)
224
236
  openai_key = getattr(tts_cfg, "openai_api_key", "") if tts_cfg else ""
225
- openai_base = ""
237
+ openai_base = getattr(tts_cfg, "openai_api_base", "") if tts_cfg else ""
238
+ tts_model = getattr(tts_cfg, "model", "") if tts_cfg else ""
226
239
  default_backend = getattr(tts_cfg, "default_backend", "edge") if tts_cfg else "edge"
227
240
  default_voice = getattr(tts_cfg, "default_voice", "") if tts_cfg else ""
228
241
 
229
- # Fallback only for OpenAI-compatible providers
230
242
  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)
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"
237
254
 
238
255
  tools.append(TTSTool(
239
256
  workspace=ws,
@@ -0,0 +1,231 @@
1
+ """Image generation tool — FAL.ai backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import threading
8
+ from typing import Any
9
+
10
+ from echo_agent.agent.tools.base import Tool, ToolExecutionContext, ToolResult
11
+
12
+ DEFAULT_MODEL = "fal-ai/flux/schnell"
13
+
14
+ FAL_MODELS: dict[str, dict[str, Any]] = {
15
+ "fal-ai/flux/schnell": {
16
+ "display": "FLUX Schnell",
17
+ "size_style": "image_size_preset",
18
+ "sizes": {
19
+ "landscape": "landscape_16_9",
20
+ "square": "square_hd",
21
+ "portrait": "portrait_16_9",
22
+ },
23
+ "defaults": {
24
+ "num_images": 1,
25
+ "output_format": "png",
26
+ "enable_safety_checker": False,
27
+ "sync_mode": True,
28
+ },
29
+ "supports": {
30
+ "prompt", "image_size", "num_images", "output_format",
31
+ "enable_safety_checker", "sync_mode", "seed",
32
+ },
33
+ },
34
+ "fal-ai/flux-2-pro": {
35
+ "display": "FLUX 2 Pro",
36
+ "size_style": "image_size_preset",
37
+ "sizes": {
38
+ "landscape": "landscape_16_9",
39
+ "square": "square_hd",
40
+ "portrait": "portrait_16_9",
41
+ },
42
+ "defaults": {
43
+ "num_inference_steps": 50,
44
+ "guidance_scale": 4.5,
45
+ "num_images": 1,
46
+ "output_format": "png",
47
+ "enable_safety_checker": False,
48
+ "sync_mode": True,
49
+ },
50
+ "supports": {
51
+ "prompt", "image_size", "num_inference_steps", "guidance_scale",
52
+ "num_images", "output_format", "enable_safety_checker",
53
+ "sync_mode", "seed",
54
+ },
55
+ },
56
+ "fal-ai/ideogram/v3": {
57
+ "display": "Ideogram V3",
58
+ "size_style": "image_size_preset",
59
+ "sizes": {
60
+ "landscape": "landscape_16_9",
61
+ "square": "square_hd",
62
+ "portrait": "portrait_16_9",
63
+ },
64
+ "defaults": {
65
+ "rendering_speed": "BALANCED",
66
+ "expand_prompt": True,
67
+ "style": "AUTO",
68
+ },
69
+ "supports": {
70
+ "prompt", "image_size", "rendering_speed", "expand_prompt",
71
+ "style", "seed",
72
+ },
73
+ },
74
+ "fal-ai/recraft/v4/pro/text-to-image": {
75
+ "display": "Recraft V4 Pro",
76
+ "size_style": "image_size_preset",
77
+ "sizes": {
78
+ "landscape": "landscape_16_9",
79
+ "square": "square_hd",
80
+ "portrait": "portrait_16_9",
81
+ },
82
+ "defaults": {
83
+ "enable_safety_checker": False,
84
+ },
85
+ "supports": {
86
+ "prompt", "image_size", "enable_safety_checker",
87
+ },
88
+ },
89
+ "fal-ai/qwen-image": {
90
+ "display": "Qwen Image",
91
+ "size_style": "image_size_preset",
92
+ "sizes": {
93
+ "landscape": "landscape_16_9",
94
+ "square": "square_hd",
95
+ "portrait": "portrait_16_9",
96
+ },
97
+ "defaults": {
98
+ "num_inference_steps": 30,
99
+ "guidance_scale": 2.5,
100
+ "num_images": 1,
101
+ "output_format": "png",
102
+ },
103
+ "supports": {
104
+ "prompt", "image_size", "num_inference_steps", "guidance_scale",
105
+ "num_images", "output_format", "seed", "sync_mode",
106
+ },
107
+ },
108
+ }
109
+
110
+ VALID_ASPECT_RATIOS = ("landscape", "square", "portrait")
111
+
112
+
113
+ def _load_fal_client():
114
+ try:
115
+ from echo_agent.dependencies import ensure
116
+ ensure("tool.image-gen-fal")
117
+ except Exception:
118
+ pass
119
+ import fal_client # type: ignore
120
+ return fal_client
121
+
122
+
123
+ _fal_env_lock = threading.Lock()
124
+
125
+
126
+ def _build_payload(model_id: str, prompt: str, aspect_ratio: str) -> dict[str, Any]:
127
+ meta = FAL_MODELS[model_id]
128
+ size_style = meta["size_style"]
129
+ sizes = meta["sizes"]
130
+
131
+ aspect = aspect_ratio if aspect_ratio in sizes else "landscape"
132
+ payload: dict[str, Any] = dict(meta.get("defaults", {}))
133
+ payload["prompt"] = prompt.strip()
134
+
135
+ if size_style == "image_size_preset":
136
+ payload["image_size"] = sizes[aspect]
137
+ elif size_style == "aspect_ratio":
138
+ payload["aspect_ratio"] = sizes[aspect]
139
+
140
+ supports = meta["supports"]
141
+ return {k: v for k, v in payload.items() if k in supports}
142
+
143
+
144
+ class FalImageGenTool(Tool):
145
+ name = "image_generate"
146
+ description = "Generate an image from a text prompt using FAL.ai (supports FLUX, Ideogram, Recraft, Qwen and more)."
147
+ parameters = {
148
+ "type": "object",
149
+ "properties": {
150
+ "prompt": {"type": "string", "description": "Text description of the image to generate."},
151
+ "aspect_ratio": {
152
+ "type": "string",
153
+ "enum": ["landscape", "square", "portrait"],
154
+ "description": "Image aspect ratio.",
155
+ },
156
+ },
157
+ "required": ["prompt"],
158
+ }
159
+ timeout_seconds = 120
160
+
161
+ def __init__(self, fal_key: str = "", model: str = ""):
162
+ self._fal_key = fal_key
163
+ self._model = model or DEFAULT_MODEL
164
+
165
+ def is_ready(self) -> bool:
166
+ return bool(self._fal_key)
167
+
168
+ def readiness_detail(self) -> tuple[bool, str]:
169
+ if self._fal_key:
170
+ return True, "ok"
171
+ return False, "FAL.ai API key not configured"
172
+
173
+ async def execute(self, params: dict[str, Any], ctx: ToolExecutionContext | None = None) -> ToolResult:
174
+ if not self._fal_key:
175
+ return ToolResult(success=False, error="FAL.ai API key not configured")
176
+
177
+ prompt = params.get("prompt", "").strip()
178
+ if not prompt:
179
+ return ToolResult(success=False, error="prompt is required")
180
+
181
+ aspect_ratio = params.get("aspect_ratio", "landscape").lower().strip()
182
+ if aspect_ratio not in VALID_ASPECT_RATIOS:
183
+ aspect_ratio = "landscape"
184
+
185
+ model_id = self._model
186
+ if model_id not in FAL_MODELS:
187
+ supported = ", ".join(sorted(FAL_MODELS.keys()))
188
+ return ToolResult(
189
+ success=False,
190
+ error=f"Unknown FAL model '{model_id}'. Supported models: {supported}",
191
+ )
192
+
193
+ arguments = _build_payload(model_id, prompt, aspect_ratio)
194
+
195
+ try:
196
+ fal = _load_fal_client()
197
+ except ImportError:
198
+ return ToolResult(
199
+ success=False,
200
+ error="fal-client package not installed. Run: pip install fal-client",
201
+ )
202
+
203
+ def _submit_with_key():
204
+ with _fal_env_lock:
205
+ prev_key = os.environ.get("FAL_KEY")
206
+ os.environ["FAL_KEY"] = self._fal_key
207
+ try:
208
+ handler = fal.submit(model_id, arguments=arguments)
209
+ return handler.get()
210
+ finally:
211
+ if prev_key is not None:
212
+ os.environ["FAL_KEY"] = prev_key
213
+ else:
214
+ os.environ.pop("FAL_KEY", None)
215
+
216
+ try:
217
+ result = await asyncio.to_thread(_submit_with_key)
218
+ except Exception as e:
219
+ return ToolResult(success=False, error=f"FAL.ai generation failed: {e}")
220
+
221
+ images = result.get("images") if isinstance(result, dict) else None
222
+ if not images:
223
+ return ToolResult(success=False, error="No images returned from FAL.ai")
224
+
225
+ image_url = images[0].get("url", "")
226
+ width = images[0].get("width", "")
227
+ height = images[0].get("height", "")
228
+ output = f"Image URL: {image_url}"
229
+ if width and height:
230
+ output += f"\nSize: {width}x{height}"
231
+ return ToolResult(output=output, metadata={"url": image_url})
@@ -21,6 +21,7 @@ class SendResult:
21
21
  success: bool
22
22
  message_id: str = ""
23
23
  error: str = ""
24
+ skipped: bool = False
24
25
 
25
26
 
26
27
  class BaseChannel(ABC):
@@ -52,6 +53,20 @@ class BaseChannel(ABC):
52
53
  async def send(self, event: OutboundEvent) -> SendResult | None:
53
54
  """Send a message through this channel."""
54
55
 
56
+ def should_deliver(self, event: OutboundEvent) -> bool:
57
+ """Return False to silently skip non-final messages on channels that cannot edit.
58
+
59
+ Channels that support message editing (Telegram, Discord) can show progress
60
+ then overwrite it with the final response. Channels without edit support would
61
+ deliver every intermediate chunk as a separate, irrevocable message — confusing
62
+ the user with duplicates. This guard prevents that at the base layer.
63
+ """
64
+ if self.supports_edit:
65
+ return True
66
+ if event.is_final:
67
+ return True
68
+ return False
69
+
55
70
  async def edit_message(
56
71
  self,
57
72
  chat_id: str,
@@ -56,6 +56,8 @@ class DingTalkChannel(BaseChannel):
56
56
  await self._session.close()
57
57
 
58
58
  async def send(self, event: OutboundEvent) -> SendResult | None:
59
+ if not self.should_deliver(event):
60
+ return SendResult(success=True, skipped=True)
59
61
  text = event.text or ""
60
62
  if not text or not self._session:
61
63
  return SendResult(success=False, error="no text or no session")
@@ -44,6 +44,8 @@ class EmailChannel(BaseChannel):
44
44
  pass
45
45
 
46
46
  async def send(self, event: OutboundEvent) -> SendResult | None:
47
+ if not self.should_deliver(event):
48
+ return SendResult(success=True, skipped=True)
47
49
  text = event.text or ""
48
50
  if not text:
49
51
  return SendResult(success=False, error="no text")
@@ -56,6 +56,8 @@ class FeishuChannel(BaseChannel):
56
56
  await self._session.close()
57
57
 
58
58
  async def send(self, event: OutboundEvent) -> SendResult | None:
59
+ if not self.should_deliver(event):
60
+ return SendResult(success=True, skipped=True)
59
61
  text = event.text or ""
60
62
  if not text or not self._session:
61
63
  return SendResult(success=False, error="no text or no session")
@@ -54,6 +54,8 @@ class MatrixChannel(BaseChannel):
54
54
  await self._session.close()
55
55
 
56
56
  async def send(self, event: OutboundEvent) -> SendResult | None:
57
+ if not self.should_deliver(event):
58
+ return SendResult(success=True, skipped=True)
57
59
  text = event.text or ""
58
60
  if not text or not self._session:
59
61
  return SendResult(success=False, error="no text or no session")
@@ -191,6 +191,8 @@ class QQBotChannel(BaseChannel):
191
191
  # ── Send ─────────────────────────────────────────────────────────────────
192
192
 
193
193
  async def send(self, event: OutboundEvent) -> SendResult | None:
194
+ if not self.should_deliver(event):
195
+ return SendResult(success=True, skipped=True)
194
196
  if not self._session:
195
197
  return SendResult(success=False, error="session not initialized")
196
198
  await self._ensure_token()
@@ -43,6 +43,8 @@ class WebhookChannel(BaseChannel):
43
43
  await self._runner.cleanup()
44
44
 
45
45
  async def send(self, event: OutboundEvent) -> SendResult | None:
46
+ if not self.should_deliver(event):
47
+ return SendResult(success=True, skipped=True)
46
48
  future = self._pending_responses.pop(event.reply_to_id or "", None)
47
49
  if future and not future.done():
48
50
  future.set_result(event.text)
@@ -55,6 +55,8 @@ class WeComChannel(BaseChannel):
55
55
  await self._session.close()
56
56
 
57
57
  async def send(self, event: OutboundEvent) -> SendResult | None:
58
+ if not self.should_deliver(event):
59
+ return SendResult(success=True, skipped=True)
58
60
  text = event.text or ""
59
61
  if not text or not self._session:
60
62
  return SendResult(success=False, error="no text or no session")
@@ -367,6 +367,8 @@ class WeixinChannel(BaseChannel):
367
367
  # ── Send ─────────────────────────────────────────────────────────────────
368
368
 
369
369
  async def send(self, event: OutboundEvent) -> SendResult | None:
370
+ if not self.should_deliver(event):
371
+ return SendResult(success=True, skipped=True)
370
372
  text = event.text or ""
371
373
  if not text or not self._send_session or not self._token:
372
374
  return SendResult(success=False, error="no text, no session, or no token")
@@ -51,6 +51,8 @@ class WhatsAppChannel(BaseChannel):
51
51
  await self._session.close()
52
52
 
53
53
  async def send(self, event: OutboundEvent) -> SendResult | None:
54
+ if not self.should_deliver(event):
55
+ return SendResult(success=True, skipped=True)
54
56
  text = event.text or ""
55
57
  if not text or not self._session:
56
58
  return SendResult(success=False, error="no text or no session")
@@ -156,9 +156,17 @@ MESSAGES = {
156
156
  "web_provider": "Search provider:",
157
157
  "web_api_key": "Search API key",
158
158
  "image_api_key": "Image-gen API key",
159
+ "image_api_base": "Image-gen API base URL",
159
160
  "image_model": "Image-gen model",
161
+ "image_backend": "Image-gen backend:",
162
+ "image_backend_openai": "OpenAI-compatible (SiliconFlow / DashScope / OpenAI)",
163
+ "image_backend_fal": "FAL.ai (FLUX / Ideogram / Recraft / Qwen)",
164
+ "image_fal_key": "FAL.ai API key",
165
+ "image_fal_model": "FAL model (e.g. fal-ai/flux/schnell)",
160
166
  "tts_backend": "TTS backend:",
161
167
  "tts_openai_key": "OpenAI TTS API key",
168
+ "tts_openai_base": "TTS API base URL",
169
+ "tts_model": "TTS model",
162
170
  "mcp_skip_hint": "Configure MCP servers later in echo-agent.yaml under tools.mcp_servers",
163
171
  "saved": "Tool profile: {profile}, optional: {extras}",
164
172
  },
@@ -156,9 +156,17 @@ MESSAGES = {
156
156
  "web_provider": "搜索 Provider:",
157
157
  "web_api_key": "搜索 API key",
158
158
  "image_api_key": "图像生成 API key",
159
+ "image_api_base": "图像生成 API 地址",
159
160
  "image_model": "图像生成模型",
161
+ "image_backend": "图像生成后端:",
162
+ "image_backend_openai": "OpenAI 兼容(硅基流动/通义/智谱/OpenAI)",
163
+ "image_backend_fal": "FAL.ai(FLUX/Ideogram/Recraft/Qwen)",
164
+ "image_fal_key": "FAL.ai API key",
165
+ "image_fal_model": "FAL 模型(如 fal-ai/flux/schnell)",
160
166
  "tts_backend": "TTS 后端:",
161
167
  "tts_openai_key": "OpenAI TTS API key",
168
+ "tts_openai_base": "TTS API 地址",
169
+ "tts_model": "TTS 模型",
162
170
  "mcp_skip_hint": "稍后在 echo-agent.yaml 的 tools.mcp_servers 中配置 MCP。",
163
171
  "saved": "工具档位:{profile},可选:{extras}",
164
172
  },
@@ -389,7 +389,7 @@ def setup_tools(config: dict) -> None:
389
389
  if (tools.get("web", {}) or {}).get("enabled"):
390
390
  pre_selected.append(TOOL_OPTIONS.index("web"))
391
391
  image_block = tools.get("image_gen") or tools.get("imageGen") or {}
392
- if image_block.get("api_key") or image_block.get("apiKey"):
392
+ if image_block.get("api_key") or image_block.get("apiKey") or image_block.get("fal_key") or image_block.get("falKey"):
393
393
  pre_selected.append(TOOL_OPTIONS.index("image_gen"))
394
394
  tts_block = tools.get("tts", {}) or {}
395
395
  if tts_block.get("openai_api_key") or tts_block.get("openaiApiKey") or tts_block.get("default_backend"):
@@ -436,16 +436,36 @@ def setup_tools(config: dict) -> None:
436
436
 
437
437
  if "image_gen" in chosen:
438
438
  ig = _ensure_dict(tools, "image_gen")
439
- existing = ig.get("api_key") or ig.get("apiKey") or ""
440
- if existing:
441
- new_key = prompt(f" {t('tools.image_api_key')} [****{t('common.saved')}]", password=True)
442
- if new_key:
443
- ig["api_key"] = new_key
439
+ backend_options = [t("tools.image_backend_openai"), t("tools.image_backend_fal")]
440
+ backend_values = ["openai", "fal"]
441
+ cur_backend = ig.get("backend", "openai")
442
+ b_idx = prompt_choice(t("tools.image_backend"), backend_options,
443
+ default=backend_values.index(cur_backend) if cur_backend in backend_values else 0)
444
+ ig["backend"] = backend_values[b_idx]
445
+
446
+ if backend_values[b_idx] == "fal":
447
+ existing = ig.get("fal_key") or ig.get("falKey") or ""
448
+ if existing:
449
+ new_key = prompt(f" {t('tools.image_fal_key')} [****{t('common.saved')}]", password=True)
450
+ if new_key:
451
+ ig["fal_key"] = new_key
452
+ else:
453
+ new_key = prompt(f" {t('tools.image_fal_key')}", password=True)
454
+ if new_key:
455
+ ig["fal_key"] = new_key
456
+ ig["fal_model"] = prompt(f" {t('tools.image_fal_model')}", default=ig.get("fal_model") or ig.get("falModel") or "fal-ai/flux/schnell")
444
457
  else:
445
- new_key = prompt(f" {t('tools.image_api_key')}", password=True)
446
- if new_key:
447
- ig["api_key"] = new_key
448
- ig["model"] = prompt(f" {t('tools.image_model')}", default=ig.get("model", "dall-e-3"))
458
+ existing = ig.get("api_key") or ig.get("apiKey") or ""
459
+ if existing:
460
+ new_key = prompt(f" {t('tools.image_api_key')} [****{t('common.saved')}]", password=True)
461
+ if new_key:
462
+ ig["api_key"] = new_key
463
+ else:
464
+ new_key = prompt(f" {t('tools.image_api_key')}", password=True)
465
+ if new_key:
466
+ ig["api_key"] = new_key
467
+ ig["api_base"] = prompt(f" {t('tools.image_api_base')}", default=ig.get("api_base") or ig.get("apiBase") or "https://api.openai.com/v1")
468
+ ig["model"] = prompt(f" {t('tools.image_model')}", default=ig.get("model", "dall-e-3"))
449
469
 
450
470
  if "tts" in chosen:
451
471
  tts = _ensure_dict(tools, "tts")
@@ -462,6 +482,8 @@ def setup_tools(config: dict) -> None:
462
482
  tts["openai_api_key"] = new_key
463
483
  else:
464
484
  tts["openai_api_key"] = prompt(f" {t('tools.tts_openai_key')}", password=True)
485
+ tts["openai_api_base"] = prompt(f" {t('tools.tts_openai_base')}", default=tts.get("openai_api_base", "https://api.openai.com/v1"))
486
+ tts["model"] = prompt(f" {t('tools.tts_model')}", default=tts.get("model", "tts-1"))
465
487
 
466
488
  code_exec = _ensure_dict(tools, "code_exec")
467
489
  code_exec["enabled"] = "code_exec" in chosen
@@ -243,13 +243,20 @@ class WebToolConfig(_Base):
243
243
 
244
244
 
245
245
  class ImageGenConfig(_Base):
246
+ backend: str = "openai"
247
+ # OpenAI-compatible backend
246
248
  api_key: str = ""
247
249
  api_base: str = ""
248
- model: str = "dall-e-3"
250
+ model: str = ""
251
+ # FAL.ai backend
252
+ fal_key: str = ""
253
+ fal_model: str = ""
249
254
 
250
255
 
251
256
  class TTSConfig(_Base):
252
257
  openai_api_key: str = ""
258
+ openai_api_base: str = ""
259
+ model: str = ""
253
260
  default_backend: str = "edge"
254
261
  default_voice: str = ""
255
262
 
@@ -75,6 +75,7 @@ SKILL_DEPS: dict[str, tuple[str, ...]] = {
75
75
 
76
76
  # ─── Creative skills ──────────────────────────────────────────────────
77
77
  "skill.image-gen": (), # uses API via urllib, no extra deps
78
+ "tool.image-gen-fal": ("fal-client>=0.5",),
78
79
  "skill.meme-gen": ("Pillow>=10.0",),
79
80
  "skill.ppt-author": ("python-pptx>=1.0",),
80
81
  "skill.excel-author": ("openpyxl>=3.1",),
@@ -0,0 +1,50 @@
1
+ """Management API routes for the desktop client.
2
+
3
+ All routes are registered under the gateway's api_prefix (default /api).
4
+ Authentication reuses the existing GatewayAuth token check.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from aiohttp import web
12
+
13
+ if TYPE_CHECKING:
14
+ from echo_agent.gateway.server import GatewayServer
15
+
16
+
17
+ def register_management_routes(app: web.Application, prefix: str, server: GatewayServer) -> None:
18
+ from echo_agent.gateway.api.memory import MemoryAPI
19
+ from echo_agent.gateway.api.skills import SkillsAPI
20
+ from echo_agent.gateway.api.channels import ChannelsAPI
21
+ from echo_agent.gateway.api.knowledge import KnowledgeAPI
22
+ from echo_agent.gateway.api.config import ConfigAPI
23
+
24
+ memory_api = MemoryAPI(server)
25
+ skills_api = SkillsAPI(server)
26
+ channels_api = ChannelsAPI(server)
27
+ knowledge_api = KnowledgeAPI(server)
28
+ config_api = ConfigAPI(server)
29
+
30
+ app.router.add_get(f"{prefix}/memory", memory_api.list_entries)
31
+ app.router.add_get(f"{prefix}/memory/stats", memory_api.stats)
32
+ app.router.add_post(f"{prefix}/memory/search", memory_api.search)
33
+ app.router.add_get(f"{prefix}/memory/{{id}}", memory_api.get_entry)
34
+ app.router.add_put(f"{prefix}/memory/{{id}}", memory_api.update_entry)
35
+ app.router.add_delete(f"{prefix}/memory/{{id}}", memory_api.delete_entry)
36
+
37
+ app.router.add_get(f"{prefix}/skills", skills_api.list_skills)
38
+ app.router.add_get(f"{prefix}/skills/{{name}}", skills_api.get_skill)
39
+ app.router.add_post(f"{prefix}/skills/{{name}}/toggle", skills_api.toggle_skill)
40
+
41
+ app.router.add_get(f"{prefix}/channels", channels_api.list_channels)
42
+
43
+ app.router.add_get(f"{prefix}/knowledge/status", knowledge_api.get_status)
44
+ app.router.add_post(f"{prefix}/knowledge/rebuild", knowledge_api.rebuild)
45
+ app.router.add_post(f"{prefix}/knowledge/upload", knowledge_api.upload)
46
+ app.router.add_get(f"{prefix}/knowledge/documents", knowledge_api.list_documents)
47
+ app.router.add_delete(f"{prefix}/knowledge/documents/{{path}}", knowledge_api.delete_document)
48
+
49
+ app.router.add_get(f"{prefix}/config", config_api.get_config)
50
+ app.router.add_get(f"{prefix}/config/models", config_api.get_models)