polyrob 0.5.0__py3-none-any.whl

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 (745) hide show
  1. agents/README.md +453 -0
  2. agents/__init__.py +164 -0
  3. agents/base_agent.py +743 -0
  4. agents/personality/character.py +119 -0
  5. agents/personality/character_manager.py +309 -0
  6. agents/personality/characters/rob.character.json +56 -0
  7. agents/personality/characters/trump.character.json +63 -0
  8. agents/personality/persona_render.py +78 -0
  9. agents/personality/persona_resolver.py +115 -0
  10. agents/prompt/__init__.py +58 -0
  11. agents/prompt/base_prompt.py +92 -0
  12. agents/prompt/system.py +417 -0
  13. agents/task/README_PATH_MANAGEMENT.md +141 -0
  14. agents/task/__init__.py +162 -0
  15. agents/task/agent/__init__.py +70 -0
  16. agents/task/agent/agent_state.py +408 -0
  17. agents/task/agent/async_delegation.py +274 -0
  18. agents/task/agent/autonomy_state.py +275 -0
  19. agents/task/agent/conversation.py +75 -0
  20. agents/task/agent/core/__init__.py +6 -0
  21. agents/task/agent/core/aux_metering.py +61 -0
  22. agents/task/agent/core/background_review.py +157 -0
  23. agents/task/agent/core/construction.py +1179 -0
  24. agents/task/agent/core/conversational_exit.py +49 -0
  25. agents/task/agent/core/correspondent_gate.py +227 -0
  26. agents/task/agent/core/curator.py +246 -0
  27. agents/task/agent/core/episodic_digest.py +103 -0
  28. agents/task/agent/core/error_recovery.py +480 -0
  29. agents/task/agent/core/history_io.py +257 -0
  30. agents/task/agent/core/llm_provisioning.py +287 -0
  31. agents/task/agent/core/llm_runner.py +547 -0
  32. agents/task/agent/core/logging_io.py +276 -0
  33. agents/task/agent/core/loop_detection.py +262 -0
  34. agents/task/agent/core/memory_prefetch.py +325 -0
  35. agents/task/agent/core/memory_writer.py +667 -0
  36. agents/task/agent/core/model_introspection.py +82 -0
  37. agents/task/agent/core/model_swap.py +177 -0
  38. agents/task/agent/core/next_action_internal.py +1331 -0
  39. agents/task/agent/core/output_validation.py +303 -0
  40. agents/task/agent/core/project_context.py +272 -0
  41. agents/task/agent/core/resources.py +93 -0
  42. agents/task/agent/core/result_processing.py +389 -0
  43. agents/task/agent/core/run_loop.py +661 -0
  44. agents/task/agent/core/safety_lifecycle.py +228 -0
  45. agents/task/agent/core/secret_guard.py +293 -0
  46. agents/task/agent/core/self_wake.py +280 -0
  47. agents/task/agent/core/session_metadata.py +104 -0
  48. agents/task/agent/core/step.py +781 -0
  49. agents/task/agent/core/step_execution.py +398 -0
  50. agents/task/agent/core/step_telemetry.py +222 -0
  51. agents/task/agent/core/test_aux_metering.py +50 -0
  52. agents/task/agent/core/test_billing_failover.py +68 -0
  53. agents/task/agent/core/test_fatal_error_classifier.py +38 -0
  54. agents/task/agent/core/test_judge_validation.py +275 -0
  55. agents/task/agent/core/test_model_swap.py +190 -0
  56. agents/task/agent/core/test_reflection_offload.py +54 -0
  57. agents/task/agent/core/turn_input.py +8 -0
  58. agents/task/agent/core/untrusted_wrap.py +103 -0
  59. agents/task/agent/core/user_ingress.py +228 -0
  60. agents/task/agent/hitl_manager.py +396 -0
  61. agents/task/agent/log_sanitize.py +36 -0
  62. agents/task/agent/message_manager/config.py +97 -0
  63. agents/task/agent/message_manager/service.py +758 -0
  64. agents/task/agent/message_manager/tests.py +305 -0
  65. agents/task/agent/message_manager/tool_call_builder.py +539 -0
  66. agents/task/agent/message_manager/tool_message_repair.py +364 -0
  67. agents/task/agent/message_manager/views.py +143 -0
  68. agents/task/agent/messages/__init__.py +8 -0
  69. agents/task/agent/messages/builders.py +309 -0
  70. agents/task/agent/messages/compactor.py +611 -0
  71. agents/task/agent/messages/context_references.py +271 -0
  72. agents/task/agent/messages/filters.py +333 -0
  73. agents/task/agent/messages/guidance.py +328 -0
  74. agents/task/agent/messages/persistence.py +448 -0
  75. agents/task/agent/messages/retrieval.py +506 -0
  76. agents/task/agent/messages/sqlite_persistence.py +85 -0
  77. agents/task/agent/messages/test_compaction_upgrade.py +349 -0
  78. agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
  79. agents/task/agent/messages/test_sqlite_persistence.py +54 -0
  80. agents/task/agent/messages/token_counter.py +532 -0
  81. agents/task/agent/orchestrator.py +776 -0
  82. agents/task/agent/profile_manager.py +105 -0
  83. agents/task/agent/profile_registry.py +173 -0
  84. agents/task/agent/prompts.py +1090 -0
  85. agents/task/agent/scenario_registry.py +226 -0
  86. agents/task/agent/service.py +509 -0
  87. agents/task/agent/session.py +984 -0
  88. agents/task/agent/skill_discovery.py +110 -0
  89. agents/task/agent/skill_frontmatter.py +94 -0
  90. agents/task/agent/skill_manager.py +1201 -0
  91. agents/task/agent/skill_store.py +627 -0
  92. agents/task/agent/skill_validation.py +56 -0
  93. agents/task/agent/skill_writer.py +492 -0
  94. agents/task/agent/sub_agent_manager.py +1137 -0
  95. agents/task/agent/test_session_disk_recovery.py +34 -0
  96. agents/task/agent/tests.py +513 -0
  97. agents/task/agent/tool_call_tracker.py +511 -0
  98. agents/task/agent/views.py +469 -0
  99. agents/task/config.py +432 -0
  100. agents/task/constants.py +1384 -0
  101. agents/task/flag_defaults.py +63 -0
  102. agents/task/goals/__init__.py +1 -0
  103. agents/task/goals/autonomy_marker.py +28 -0
  104. agents/task/goals/board.py +690 -0
  105. agents/task/goals/completion_judge.py +247 -0
  106. agents/task/goals/context.py +62 -0
  107. agents/task/goals/dispatcher.py +848 -0
  108. agents/task/goals/escalation.py +102 -0
  109. agents/task/goals/planner.py +114 -0
  110. agents/task/logging_config.py +155 -0
  111. agents/task/path.py +991 -0
  112. agents/task/robust_parse_config.py +400 -0
  113. agents/task/runtime/__init__.py +0 -0
  114. agents/task/runtime/run_as_session.py +130 -0
  115. agents/task/runtime_safety.py +308 -0
  116. agents/task/session/__init__.py +5 -0
  117. agents/task/session/browser_pool.py +130 -0
  118. agents/task/session/cleanup.py +612 -0
  119. agents/task/session/execution.py +397 -0
  120. agents/task/session/feed.py +200 -0
  121. agents/task/session/hitl_ingress.py +201 -0
  122. agents/task/session/hooks.py +83 -0
  123. agents/task/session/multi_agent.py +243 -0
  124. agents/task/session/workspace.py +91 -0
  125. agents/task/session_registry.py +91 -0
  126. agents/task/session_route.py +35 -0
  127. agents/task/sqlite_session_registry.py +261 -0
  128. agents/task/surface_config.py +185 -0
  129. agents/task/telemetry/__init__.py +58 -0
  130. agents/task/telemetry/event_log.py +180 -0
  131. agents/task/telemetry/formatters.py +642 -0
  132. agents/task/telemetry/manager.py +571 -0
  133. agents/task/telemetry/memory_events.py +66 -0
  134. agents/task/telemetry/self_events.py +53 -0
  135. agents/task/telemetry/sequence.py +160 -0
  136. agents/task/telemetry/service.py +1265 -0
  137. agents/task/telemetry/views.py +882 -0
  138. agents/task/templates.py +98 -0
  139. agents/task/test_aux_chain.py +151 -0
  140. agents/task/test_aux_router.py +41 -0
  141. agents/task/tests/test_parse_robustness.py +170 -0
  142. agents/task/tool_defaults.py +110 -0
  143. agents/task/utils.py +762 -0
  144. agents/task/utils_json.py +768 -0
  145. agents/task/utils_webview.py +86 -0
  146. agents/task/workspace_context.py +229 -0
  147. agents/task_agent_lite.py +2398 -0
  148. api/README.md +683 -0
  149. api/TASK_API_DOCS.md +151 -0
  150. api/__init__.py +0 -0
  151. api/a2a/__init__.py +37 -0
  152. api/a2a/agent_card.py +337 -0
  153. api/a2a/client.py +657 -0
  154. api/a2a/endpoints.py +375 -0
  155. api/a2a/models.py +268 -0
  156. api/a2a/streaming.py +419 -0
  157. api/a2a/task_handler.py +929 -0
  158. api/admin_endpoints.py +1584 -0
  159. api/app.py +923 -0
  160. api/auth_constants.py +28 -0
  161. api/auth_endpoints.py +402 -0
  162. api/auth_state.py +36 -0
  163. api/chat_via_task.py +42 -0
  164. api/conversation_manager.py +231 -0
  165. api/dependencies.py +186 -0
  166. api/eip8004_endpoints.py +452 -0
  167. api/hyperliquid_models.py +174 -0
  168. api/hyperliquid_routes.py +450 -0
  169. api/interfaces.py +207 -0
  170. api/jwt_middleware.py +83 -0
  171. api/kb/__init__.py +1 -0
  172. api/kb/endpoints.py +314 -0
  173. api/mcp_models.py +279 -0
  174. api/mcp_routes.py +438 -0
  175. api/middleware.py +526 -0
  176. api/models.py +176 -0
  177. api/openai_compat/__init__.py +0 -0
  178. api/openai_compat/model_map.py +25 -0
  179. api/openai_compat/models.py +59 -0
  180. api/openai_compat/router.py +140 -0
  181. api/payment_endpoints.py +444 -0
  182. api/payment_verification.py +138 -0
  183. api/polymarket_models.py +155 -0
  184. api/polymarket_routes.py +501 -0
  185. api/pricing_endpoints.py +89 -0
  186. api/session_routing.py +54 -0
  187. api/skill_endpoints.py +453 -0
  188. api/task_http_api.py +1856 -0
  189. api/webhooks.py +52 -0
  190. api/x402_endpoints.py +280 -0
  191. avatar/README.md +44 -0
  192. avatar/__init__.py +1 -0
  193. avatar/config/rob.json +7 -0
  194. avatar/mindprint.js +395 -0
  195. avatar/renders/rob.meta.json +20 -0
  196. avatar/renders/rob.png +0 -0
  197. avatar/studio.html +373 -0
  198. avatar/webview/avatar-live.js +44 -0
  199. cli/README.md +89 -0
  200. cli/__init__.py +0 -0
  201. cli/cli_surface.py +67 -0
  202. cli/commands/__init__.py +0 -0
  203. cli/commands/_bootstrap.py +49 -0
  204. cli/commands/_errors.py +53 -0
  205. cli/commands/chat.py +973 -0
  206. cli/commands/config.py +100 -0
  207. cli/commands/dashboard.py +103 -0
  208. cli/commands/doctor.py +222 -0
  209. cli/commands/email.py +174 -0
  210. cli/commands/gateway.py +346 -0
  211. cli/commands/goals.py +509 -0
  212. cli/commands/init.py +276 -0
  213. cli/commands/journey.py +30 -0
  214. cli/commands/kb.py +280 -0
  215. cli/commands/model.py +107 -0
  216. cli/commands/owner.py +431 -0
  217. cli/commands/pfp.py +232 -0
  218. cli/commands/run.py +414 -0
  219. cli/commands/serve.py +41 -0
  220. cli/commands/session.py +684 -0
  221. cli/commands/skill_install.py +786 -0
  222. cli/commands/skills.py +128 -0
  223. cli/commands/subagents.py +100 -0
  224. cli/commands/surface.py +81 -0
  225. cli/commands/telegram.py +185 -0
  226. cli/commands/todos.py +209 -0
  227. cli/commands/tools.py +102 -0
  228. cli/commands/update.py +316 -0
  229. cli/commands/whatsapp.py +175 -0
  230. cli/config_store.py +276 -0
  231. cli/gitignore.py +43 -0
  232. cli/inventory.py +46 -0
  233. cli/keys.py +101 -0
  234. cli/persona.py +17 -0
  235. cli/polyrob.py +137 -0
  236. cli/ui/__init__.py +64 -0
  237. cli/ui/activity.py +141 -0
  238. cli/ui/app.py +538 -0
  239. cli/ui/banner.py +233 -0
  240. cli/ui/blocks.py +534 -0
  241. cli/ui/bootstrap_notice.py +47 -0
  242. cli/ui/commands/__init__.py +41 -0
  243. cli/ui/commands/h_cron.py +64 -0
  244. cli/ui/commands/h_journey.py +164 -0
  245. cli/ui/commands/h_kb.py +121 -0
  246. cli/ui/commands/h_learn.py +100 -0
  247. cli/ui/commands/h_mcp.py +215 -0
  248. cli/ui/commands/h_self.py +113 -0
  249. cli/ui/commands/h_skills.py +314 -0
  250. cli/ui/commands/handlers.py +1672 -0
  251. cli/ui/commands/registry.py +426 -0
  252. cli/ui/dialog.py +425 -0
  253. cli/ui/event_registry.py +110 -0
  254. cli/ui/event_specs.py +45 -0
  255. cli/ui/events.py +433 -0
  256. cli/ui/identity.py +15 -0
  257. cli/ui/lifecycle.py +159 -0
  258. cli/ui/live_hooks.py +43 -0
  259. cli/ui/model_selector.py +532 -0
  260. cli/ui/persistent_loop.py +181 -0
  261. cli/ui/pick.py +113 -0
  262. cli/ui/plain_renderer.py +351 -0
  263. cli/ui/renderer.py +384 -0
  264. cli/ui/rich_renderer.py +533 -0
  265. cli/ui/secrets.py +115 -0
  266. cli/ui/state.py +376 -0
  267. cli/ui/statusbar.py +231 -0
  268. cli/ui/streaming.py +254 -0
  269. cli/ui/terminal_render.py +105 -0
  270. cli/ui/theme.py +132 -0
  271. cli/update/__init__.py +6 -0
  272. cli/update/context.py +121 -0
  273. cli/update/detect.py +176 -0
  274. cli/update/engine.py +95 -0
  275. cli/update/migrate_guarded.py +55 -0
  276. cli/update/process_guard.py +215 -0
  277. cli/update/runners.py +78 -0
  278. cli/update/snapshot.py +270 -0
  279. cli/update/versions.py +205 -0
  280. core/README.md +377 -0
  281. core/__init__.py +174 -0
  282. core/assets.py +87 -0
  283. core/async_bridge.py +76 -0
  284. core/autonomy_runtime.py +259 -0
  285. core/base_component.py +302 -0
  286. core/bootstrap.py +681 -0
  287. core/bot.py +310 -0
  288. core/config.py +833 -0
  289. core/constants.py +126 -0
  290. core/container.py +462 -0
  291. core/db_manifest.py +101 -0
  292. core/embedding.py +57 -0
  293. core/env.py +39 -0
  294. core/exceptions.py +287 -0
  295. core/flags.py +144 -0
  296. core/flags_catalog.py +345 -0
  297. core/home_migration.py +62 -0
  298. core/identity.py +118 -0
  299. core/initialization.py +1004 -0
  300. core/instance.py +537 -0
  301. core/interactive_gate.py +96 -0
  302. core/logging.py +436 -0
  303. core/owner_doc_writer.py +176 -0
  304. core/pairing.py +193 -0
  305. core/path_safety.py +20 -0
  306. core/paths.py +27 -0
  307. core/payment_config.py +21 -0
  308. core/permissions.py +322 -0
  309. core/runtime_config.py +96 -0
  310. core/runtime_paths.py +135 -0
  311. core/seams.py +35 -0
  312. core/secret_scan.py +25 -0
  313. core/secret_scrub.py +71 -0
  314. core/secrets.py +35 -0
  315. core/security_logging_filter.py +148 -0
  316. core/self_context_writer.py +333 -0
  317. core/self_evolution.py +294 -0
  318. core/session_context.py +47 -0
  319. core/sqlite_util.py +59 -0
  320. core/surfaces/__init__.py +16 -0
  321. core/surfaces/access.py +118 -0
  322. core/surfaces/binding.py +88 -0
  323. core/surfaces/bootstrap.py +78 -0
  324. core/surfaces/circuit.py +168 -0
  325. core/surfaces/continuity.py +77 -0
  326. core/surfaces/correspondents.py +228 -0
  327. core/surfaces/dispatcher.py +216 -0
  328. core/surfaces/envelopes.py +75 -0
  329. core/surfaces/gc.py +30 -0
  330. core/surfaces/idempotency.py +57 -0
  331. core/surfaces/inbound_webhook.py +124 -0
  332. core/surfaces/media.py +32 -0
  333. core/surfaces/message_router.py +91 -0
  334. core/surfaces/outbound_allowlist.py +80 -0
  335. core/surfaces/outbound_dispatcher.py +110 -0
  336. core/surfaces/outbound_mirror.py +37 -0
  337. core/surfaces/outbound_queue.py +107 -0
  338. core/surfaces/outbound_target.py +12 -0
  339. core/surfaces/owner_admin.py +29 -0
  340. core/surfaces/proactive.py +78 -0
  341. core/surfaces/progress.py +108 -0
  342. core/surfaces/rate_bucket.py +21 -0
  343. core/surfaces/registry.py +51 -0
  344. core/surfaces/rendering.py +53 -0
  345. core/surfaces/send_policy.py +14 -0
  346. core/surfaces/serialize.py +24 -0
  347. core/surfaces/session_chat_registry.py +112 -0
  348. core/surfaces/session_policy.py +60 -0
  349. core/surfaces/surface.py +288 -0
  350. core/surfaces/transcription.py +88 -0
  351. core/surfaces/voice_echo.py +26 -0
  352. core/surfaces/voice_guard.py +23 -0
  353. core/tickers.py +174 -0
  354. core/tool_catalog.py +192 -0
  355. core/version.py +75 -0
  356. core/wallet/__init__.py +5 -0
  357. core/wallet/agent_wallet.py +59 -0
  358. core/wallet/audit_sink.py +61 -0
  359. core/wallet/config.py +70 -0
  360. core/wallet/factory.py +87 -0
  361. core/wallet/policy.py +104 -0
  362. core/wallet/signer.py +51 -0
  363. cron/README.md +65 -0
  364. cron/__init__.py +0 -0
  365. cron/delivery.py +277 -0
  366. cron/digest.py +124 -0
  367. cron/jobs.py +207 -0
  368. cron/runner.py +297 -0
  369. cron/schedule.py +194 -0
  370. cron/scheduler.py +201 -0
  371. cron/service.py +57 -0
  372. cron/wake_gate.py +223 -0
  373. data/__init__.py +1 -0
  374. data/prompts/autov2_prompts.json +87 -0
  375. data/prompts/skills/browser-automation/SKILL.md +43 -0
  376. data/prompts/skills/coding-workflow/SKILL.md +36 -0
  377. data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
  378. data/prompts/skills/document-writing/SKILL.md +33 -0
  379. data/prompts/skills/email-comms/SKILL.md +33 -0
  380. data/prompts/skills/file-data-ops/SKILL.md +35 -0
  381. data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
  382. data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
  383. data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
  384. data/prompts/skills/lead-research/SKILL.md +86 -0
  385. data/prompts/skills/market-research-brief/SKILL.md +104 -0
  386. data/prompts/skills/person-analyzer/SKILL.md +124 -0
  387. data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
  388. data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
  389. data/prompts/skills/polymarket-trading/SKILL.md +55 -0
  390. data/prompts/skills/presentation-creator/SKILL.md +36 -0
  391. data/prompts/skills/project-analyzer/SKILL.md +169 -0
  392. data/prompts/skills/rules.json +268 -0
  393. data/prompts/skills/secret-handling/SKILL.md +20 -0
  394. data/prompts/skills/skill-authoring/SKILL.md +31 -0
  395. data/prompts/skills/skill-security-review/SKILL.md +27 -0
  396. data/prompts/skills/social-discovery/SKILL.md +57 -0
  397. data/prompts/skills/task-planning/SKILL.md +33 -0
  398. data/prompts/skills/web-research/SKILL.md +39 -0
  399. data/prompts/skills/web-scraping/SKILL.md +73 -0
  400. data/prompts/skills/x-engagement/SKILL.md +69 -0
  401. data/prompts/system_prompts.json +4 -0
  402. data/subscription_channels.json +5 -0
  403. modules/README.md +503 -0
  404. modules/__init__.py +124 -0
  405. modules/auth/__init__.py +13 -0
  406. modules/auth/api_key_manager.py +157 -0
  407. modules/auth/identity_mapper.py +380 -0
  408. modules/auth/siwe_auth.py +273 -0
  409. modules/auth/tier_manager.py +173 -0
  410. modules/base_module.py +190 -0
  411. modules/credits/__init__.py +29 -0
  412. modules/credits/balance_manager.py +237 -0
  413. modules/credits/cost_utils.py +129 -0
  414. modules/credits/pricing.py +139 -0
  415. modules/credits/unified_ledger.py +148 -0
  416. modules/credits/usage_meter.py +310 -0
  417. modules/credits/usage_tracker.py +674 -0
  418. modules/database/__init__.py +33 -0
  419. modules/database/audit_log.py +322 -0
  420. modules/database/auth_tables.py +276 -0
  421. modules/database/connection.py +644 -0
  422. modules/database/connection_pool.py +305 -0
  423. modules/database/conversation_contexts.py +446 -0
  424. modules/database/database_manager.py +237 -0
  425. modules/database/hyperliquid.py +375 -0
  426. modules/database/polymarket.py +574 -0
  427. modules/database/user_mcp_servers.py +728 -0
  428. modules/database/user_profiles.py +562 -0
  429. modules/database/utils.py +107 -0
  430. modules/database/x402_tables.py +92 -0
  431. modules/eip8004/README.md +1069 -0
  432. modules/eip8004/__init__.py +65 -0
  433. modules/eip8004/contracts.py +498 -0
  434. modules/eip8004/models.py +271 -0
  435. modules/eip8004/registration.py +148 -0
  436. modules/eip8004/reputation.py +367 -0
  437. modules/eip8004/validation.py +277 -0
  438. modules/llm/__init__.py +97 -0
  439. modules/llm/adapters.py +963 -0
  440. modules/llm/anthropic_client.py +831 -0
  441. modules/llm/available_models.py +106 -0
  442. modules/llm/brain_scrubber.py +137 -0
  443. modules/llm/cache_hints.py +162 -0
  444. modules/llm/deepseek_client.py +562 -0
  445. modules/llm/gemini_client.py +1367 -0
  446. modules/llm/llm_client.py +1139 -0
  447. modules/llm/llm_client_registry.py +188 -0
  448. modules/llm/llm_factory.py +237 -0
  449. modules/llm/llm_manager.py +1036 -0
  450. modules/llm/messages.py +374 -0
  451. modules/llm/model_registry.py +1935 -0
  452. modules/llm/nvidia_client.py +125 -0
  453. modules/llm/openai_client.py +800 -0
  454. modules/llm/openrouter_client.py +969 -0
  455. modules/llm/profiles.py +178 -0
  456. modules/llm/test_anthropic_cache_capture.py +35 -0
  457. modules/llm/test_cached_pricing_derivation.py +24 -0
  458. modules/llm/think_scrubber.py +444 -0
  459. modules/llm/token_counter.py +268 -0
  460. modules/memory/__init__.py +35 -0
  461. modules/memory/backend_factory.py +83 -0
  462. modules/memory/cache_manager.py +186 -0
  463. modules/memory/episodic.py +108 -0
  464. modules/memory/local_vector_memory_provider.py +552 -0
  465. modules/memory/memory_manager.py +225 -0
  466. modules/memory/models.py +312 -0
  467. modules/memory/provider.py +210 -0
  468. modules/memory/registry.py +205 -0
  469. modules/memory/sqlite_memory_provider.py +767 -0
  470. modules/memory/task/__init__.py +52 -0
  471. modules/memory/task/compaction_manager.py +157 -0
  472. modules/memory/task/context_retriever.py +809 -0
  473. modules/memory/task/hierarchical_memory.py +1002 -0
  474. modules/memory/task/lexical_retriever.py +71 -0
  475. modules/memory/task/null_context_manager.py +213 -0
  476. modules/memory/task/phase_manager.py +625 -0
  477. modules/memory/task/reflection_service.py +157 -0
  478. modules/memory/task/semantic_retriever.py +218 -0
  479. modules/memory/task/task_context_manager.py +1165 -0
  480. modules/memory/task/test_forgetting_engages.py +24 -0
  481. modules/memory/task/test_reflection_llm.py +39 -0
  482. modules/memory/task/test_threat_scan.py +53 -0
  483. modules/memory/task/threat_scan.py +90 -0
  484. modules/memory/test_sqlite_memory_provider.py +106 -0
  485. modules/memory/user_profile_manager.py +527 -0
  486. modules/payments/__init__.py +11 -0
  487. modules/payments/deposit_monitor.py +478 -0
  488. modules/payments/price_oracle.py +70 -0
  489. modules/payments/treasury_sweeper.py +401 -0
  490. modules/payments/wallet_generator.py +145 -0
  491. modules/pfp/__init__.py +1 -0
  492. modules/pfp/config.py +92 -0
  493. modules/pfp/mesh.py +565 -0
  494. modules/pfp/push.py +69 -0
  495. modules/pfp/renderer.py +85 -0
  496. modules/pfp/store.py +82 -0
  497. modules/skills/__init__.py +1 -0
  498. modules/skills/skill_usage.py +248 -0
  499. modules/transcription/__init__.py +24 -0
  500. modules/transcription/base.py +20 -0
  501. modules/transcription/faster_whisper_transcriber.py +55 -0
  502. modules/x402/README.md +249 -0
  503. modules/x402/__init__.py +22 -0
  504. modules/x402/invoicing.py +416 -0
  505. modules/x402/middleware.py +364 -0
  506. modules/x402/settlement_watcher.py +145 -0
  507. modules/x402/x402_integration.py +344 -0
  508. polyrob-0.5.0.dist-info/METADATA +482 -0
  509. polyrob-0.5.0.dist-info/RECORD +745 -0
  510. polyrob-0.5.0.dist-info/WHEEL +5 -0
  511. polyrob-0.5.0.dist-info/entry_points.txt +2 -0
  512. polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
  513. polyrob-0.5.0.dist-info/top_level.txt +12 -0
  514. surfaces/README.md +70 -0
  515. surfaces/__init__.py +0 -0
  516. surfaces/email/__init__.py +0 -0
  517. surfaces/email/dedup.py +44 -0
  518. surfaces/email/harness.py +187 -0
  519. surfaces/email/inbound.py +159 -0
  520. surfaces/email/seed.py +60 -0
  521. surfaces/email/surface.py +105 -0
  522. surfaces/telegram/__init__.py +0 -0
  523. surfaces/telegram/dedup.py +83 -0
  524. surfaces/telegram/harness.py +886 -0
  525. surfaces/telegram/inbound.py +145 -0
  526. surfaces/telegram/interactive_tools.py +49 -0
  527. surfaces/telegram/markdown.py +347 -0
  528. surfaces/telegram/rate_limit.py +167 -0
  529. surfaces/telegram/surface.py +187 -0
  530. surfaces/telegram/voice.py +52 -0
  531. surfaces/whatsapp/__init__.py +0 -0
  532. surfaces/whatsapp/client.py +51 -0
  533. surfaces/whatsapp/harness.py +62 -0
  534. surfaces/whatsapp/inbound.py +132 -0
  535. surfaces/whatsapp/surface.py +64 -0
  536. surfaces/whatsapp/window.py +34 -0
  537. tools/README.md +578 -0
  538. tools/__init__.py +424 -0
  539. tools/alchemy/__init__.py +5 -0
  540. tools/alchemy/alchemy_tool.py +389 -0
  541. tools/anysite/__init__.py +17 -0
  542. tools/anysite/client.py +88 -0
  543. tools/anysite/tool.py +89 -0
  544. tools/base_tool.py +468 -0
  545. tools/browser/__init__.py +22 -0
  546. tools/browser/actions.py +118 -0
  547. tools/browser/browser.py +1457 -0
  548. tools/browser/browser_manager.py +690 -0
  549. tools/browser/context.py +1748 -0
  550. tools/browser/playwright_utils.py +137 -0
  551. tools/browser/views.py +56 -0
  552. tools/code_exec/SANDBOX_SECURITY.md +144 -0
  553. tools/code_exec/__init__.py +178 -0
  554. tools/code_exec/backend.py +73 -0
  555. tools/code_exec/backends/__init__.py +1 -0
  556. tools/code_exec/backends/docker.py +779 -0
  557. tools/code_exec/backends/local_subprocess.py +154 -0
  558. tools/code_exec/env_policy.py +45 -0
  559. tools/code_exec/result.py +42 -0
  560. tools/code_exec/sandbox_guard.py +59 -0
  561. tools/code_exec/tool.py +227 -0
  562. tools/coding/__init__.py +50 -0
  563. tools/coding/edit.py +107 -0
  564. tools/coding/search.py +113 -0
  565. tools/coding/tool.py +353 -0
  566. tools/collabland/__init__.py +5 -0
  567. tools/collabland/collabland_tool.py +471 -0
  568. tools/controller/__init__.py +0 -0
  569. tools/controller/_helpers.py +165 -0
  570. tools/controller/action_registration.py +1765 -0
  571. tools/controller/approval.py +244 -0
  572. tools/controller/approval_interactive.py +90 -0
  573. tools/controller/delegation.py +195 -0
  574. tools/controller/execution.py +797 -0
  575. tools/controller/execution_context.py +141 -0
  576. tools/controller/hooks.py +135 -0
  577. tools/controller/introspection.py +344 -0
  578. tools/controller/mcp_registrar.py +190 -0
  579. tools/controller/message_send.py +31 -0
  580. tools/controller/registry/__init__.py +4 -0
  581. tools/controller/registry/schema_generators.py +395 -0
  582. tools/controller/registry/schema_sanitizer.py +406 -0
  583. tools/controller/registry/service.py +1266 -0
  584. tools/controller/registry/views.py +151 -0
  585. tools/controller/service.py +409 -0
  586. tools/controller/tool_management.py +334 -0
  587. tools/controller/types.py +28 -0
  588. tools/controller/views.py +450 -0
  589. tools/cronjob_tools.py +166 -0
  590. tools/crypto_trade_gate.py +59 -0
  591. tools/descriptors.py +452 -0
  592. tools/dom/__init__.py +0 -0
  593. tools/dom/history_tree_processor/service.py +244 -0
  594. tools/dom/history_tree_processor/view.py +99 -0
  595. tools/dom/service.py +471 -0
  596. tools/dom/views.py +346 -0
  597. tools/email_tool.py +403 -0
  598. tools/exceptions.py +169 -0
  599. tools/filesystem.py +1038 -0
  600. tools/filesystem_docproc.py +331 -0
  601. tools/filesystem_pdf.py +755 -0
  602. tools/git/__init__.py +42 -0
  603. tools/git/tool.py +258 -0
  604. tools/github/__init__.py +33 -0
  605. tools/github/client.py +94 -0
  606. tools/github/tool.py +237 -0
  607. tools/goal_tools.py +309 -0
  608. tools/hyperliquid/__init__.py +56 -0
  609. tools/hyperliquid/models.py +233 -0
  610. tools/hyperliquid/service.py +1680 -0
  611. tools/knowledge_ingest.py +950 -0
  612. tools/mcp/README.md +535 -0
  613. tools/mcp/__init__.py +52 -0
  614. tools/mcp/catalog.py +115 -0
  615. tools/mcp/config.py +337 -0
  616. tools/mcp/mcp_tool.py +1415 -0
  617. tools/mcp/param_coercion.py +289 -0
  618. tools/mcp/protocol.py +1520 -0
  619. tools/mcp/rate_limit.py +50 -0
  620. tools/mcp/security.py +424 -0
  621. tools/mcp/self_install.py +111 -0
  622. tools/mcp/server_manager.py +829 -0
  623. tools/mcp/subscriptions.py +72 -0
  624. tools/mcp/user_mcp_service.py +899 -0
  625. tools/mcp/validation_tracker.py +89 -0
  626. tools/mcp/views.py +346 -0
  627. tools/oauth/__init__.py +17 -0
  628. tools/oauth/manager.py +75 -0
  629. tools/oauth/provider.py +69 -0
  630. tools/oauth/providers/__init__.py +1 -0
  631. tools/oauth/providers/generic_oauth2.py +96 -0
  632. tools/perplexity_tool.py +280 -0
  633. tools/polymarket/__init__.py +41 -0
  634. tools/polymarket/clob_adapter.py +76 -0
  635. tools/polymarket/models.py +182 -0
  636. tools/polymarket/service.py +2082 -0
  637. tools/self_env/__init__.py +61 -0
  638. tools/self_env/tool.py +274 -0
  639. tools/shell/__init__.py +80 -0
  640. tools/shell/backend_pool.py +118 -0
  641. tools/shell/discipline.py +62 -0
  642. tools/shell/executor.py +157 -0
  643. tools/shell/loopback_allow.py +87 -0
  644. tools/shell/process_registry.py +113 -0
  645. tools/shell/process_tool.py +131 -0
  646. tools/shell/state.py +129 -0
  647. tools/shell/tool.py +151 -0
  648. tools/task_tool.py +637 -0
  649. tools/twitter_tool.py +2086 -0
  650. tools/user_directory.py +179 -0
  651. tools/web_fetch/__init__.py +3 -0
  652. tools/web_fetch/fetcher.py +144 -0
  653. tools/web_fetch/render.py +44 -0
  654. tools/web_fetch/tool.py +60 -0
  655. tools/x402/__init__.py +60 -0
  656. tools/x402/client.py +47 -0
  657. tools/x402/invoice_tool.py +143 -0
  658. tools/x402/real_client.py +250 -0
  659. tools/x402/service.py +131 -0
  660. utils/README.md +498 -0
  661. utils/__init__.py +46 -0
  662. utils/auth_utils.py +114 -0
  663. utils/bounded_collections.py +162 -0
  664. utils/circuit_breaker.py +389 -0
  665. utils/gif_utils.py +435 -0
  666. utils/markdown_utils.py +291 -0
  667. utils/message_utils.py +147 -0
  668. utils/metrics.py +36 -0
  669. utils/path_validator.py +227 -0
  670. utils/rate_limit_manager.py +399 -0
  671. utils/result_size.py +296 -0
  672. utils/time_utils.py +206 -0
  673. utils/user_utils.py +188 -0
  674. webview/README.md +111 -0
  675. webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
  676. webview/__init__.py +8 -0
  677. webview/activity.py +636 -0
  678. webview/owner_auth.py +108 -0
  679. webview/pages.py +448 -0
  680. webview/repair_sessions.py +399 -0
  681. webview/server.py +4323 -0
  682. webview/server_launcher.py +112 -0
  683. webview/static/css/activity.css +89 -0
  684. webview/static/css/chat.css +1772 -0
  685. webview/static/css/components.css +1190 -0
  686. webview/static/css/config-panel.css +651 -0
  687. webview/static/css/pages/admin.css +766 -0
  688. webview/static/css/pages/profile.css +231 -0
  689. webview/static/css/pages/settings.css +297 -0
  690. webview/static/css/pages/signin.css +316 -0
  691. webview/static/css/style.css +4250 -0
  692. webview/static/css/variables.css +147 -0
  693. webview/static/css/workspace-fullwidth-fix.css +109 -0
  694. webview/static/img/favicon.ico +0 -0
  695. webview/static/js/activity.js +394 -0
  696. webview/static/js/admin/activity.js +186 -0
  697. webview/static/js/admin/dashboard.js +163 -0
  698. webview/static/js/admin/user_detail.js +400 -0
  699. webview/static/js/admin/users.js +174 -0
  700. webview/static/js/admin/utils.js +214 -0
  701. webview/static/js/chat.js +3178 -0
  702. webview/static/js/config-panel.js +522 -0
  703. webview/static/js/constants.js +20 -0
  704. webview/static/js/error-handler.js +226 -0
  705. webview/static/js/ethers.min.js +1 -0
  706. webview/static/js/event-filter.js +137 -0
  707. webview/static/js/event-store.js +351 -0
  708. webview/static/js/file-attachments.js +374 -0
  709. webview/static/js/file-loader.js +163 -0
  710. webview/static/js/index.js +103 -0
  711. webview/static/js/performance-utils.js +196 -0
  712. webview/static/js/profile.js +396 -0
  713. webview/static/js/screenshot.js +386 -0
  714. webview/static/js/session.js +1527 -0
  715. webview/static/js/settings.js +1288 -0
  716. webview/static/js/sidebar-data.js +766 -0
  717. webview/static/js/sidebar-toggle.js +296 -0
  718. webview/static/js/socket.io.min.js +7 -0
  719. webview/static/js/stats.js +355 -0
  720. webview/static/js/ui-utils.js +420 -0
  721. webview/static/js/workspace.js +966 -0
  722. webview/stats_service.py +613 -0
  723. webview/templates/__init__.py +1 -0
  724. webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  725. webview/templates/activity.html +36 -0
  726. webview/templates/admin/activity.html +122 -0
  727. webview/templates/admin/dashboard.html +112 -0
  728. webview/templates/admin/user_detail.html +228 -0
  729. webview/templates/admin/users.html +105 -0
  730. webview/templates/autonomy.html +60 -0
  731. webview/templates/error.html +133 -0
  732. webview/templates/finance.html +90 -0
  733. webview/templates/identity.html +73 -0
  734. webview/templates/index.html +42 -0
  735. webview/templates/layout.html +125 -0
  736. webview/templates/memory.html +59 -0
  737. webview/templates/owner_login.html +15 -0
  738. webview/templates/profile.html +121 -0
  739. webview/templates/session.html +312 -0
  740. webview/templates/settings.html +534 -0
  741. webview/templates/sidebar.html +112 -0
  742. webview/templates/signin.html +213 -0
  743. webview/templates/status.html +14 -0
  744. webview/templates/system.html +45 -0
  745. webview/webgate.py +196 -0
@@ -0,0 +1,190 @@
1
+ """MCP → direct-action registration (Item 7H — extracted from ``controller/service.py``).
2
+
3
+ LLMs consistently fail to use nested ``mcp_execute_tool(arguments={...})``. This
4
+ registrar flattens each discovered MCP tool into a direct action named
5
+ ``{server}_{tool}`` with a dynamic Pydantic param model built from the tool's JSON
6
+ input schema — eliminating the nesting problem.
7
+
8
+ It holds a back-reference to the owning ``Controller`` so ``registry`` and ``logger``
9
+ stay live (the Controller delegates ``_register_mcp_tools_as_direct_actions`` /
10
+ ``_create_param_model_from_schema`` here). The WS-B5 typed-array behaviour (arrays
11
+ carry their item type; objects stay lenient ``dict``) is preserved verbatim.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Dict, Type
16
+
17
+ from pydantic import BaseModel
18
+
19
+
20
+ class MCPActionRegistrar:
21
+ """Registers MCP server tools as flat Controller actions + builds param models."""
22
+
23
+ def __init__(self, controller: Any) -> None:
24
+ self._controller = controller
25
+
26
+ @property
27
+ def registry(self):
28
+ return self._controller.registry
29
+
30
+ @property
31
+ def logger(self):
32
+ return self._controller.logger
33
+
34
+ async def register(self, mcp_tool: Any) -> None:
35
+ """Register individual MCP tools as direct actions with flat parameter schemas.
36
+
37
+ Args:
38
+ mcp_tool: The MCPTool instance with server_manager
39
+ """
40
+ self.logger.info("🔧 _register_mcp_tools_as_direct_actions called")
41
+
42
+ if not hasattr(mcp_tool, 'server_manager') or not mcp_tool.server_manager:
43
+ self.logger.warning("MCP tool has no server_manager, cannot register direct actions")
44
+ return
45
+
46
+ try:
47
+ # Get all MCP tools from all servers
48
+ all_tools = mcp_tool.server_manager.get_all_tools()
49
+ self.logger.info(f"🔧 Found {len(all_tools)} MCP servers with tools")
50
+ registered_count = 0
51
+
52
+ for server_name, tools in all_tools.items():
53
+ for tool_meta in tools:
54
+ try:
55
+ # Create namespaced action name: server_toolname
56
+ action_name = f"{server_name}_{tool_meta.name}"
57
+
58
+ # Skip if already registered (avoid duplicates with mcp_* wrapper actions)
59
+ if action_name in self.registry.actions:
60
+ self.logger.debug(f"MCP action '{action_name}' already registered, skipping")
61
+ continue
62
+
63
+ # Create a dynamic Pydantic model from the tool's input schema
64
+ param_model = self.create_param_model(
65
+ action_name,
66
+ tool_meta.input_schema or {}
67
+ )
68
+
69
+ # Create an async executor that routes to execute_mcp_tool
70
+ # Use closure to capture server_name and tool_name
71
+ def make_executor(srv_name, t_name, mcp_ref):
72
+ # execution_context is a NAMED param (not **kwargs) so the
73
+ # registry routes the per-call context here instead of folding
74
+ # it into the MCP params dict — and so it reaches the per-tenant
75
+ # rate limiter (was falling back to a shared "global" bucket,
76
+ # cross-keying tenants).
77
+ async def executor(params_obj=None, execution_context=None, **kwargs):
78
+ # Convert Pydantic model to dict if needed
79
+ if params_obj is not None:
80
+ if hasattr(params_obj, 'model_dump'):
81
+ params = params_obj.model_dump()
82
+ elif isinstance(params_obj, dict):
83
+ params = params_obj
84
+ else:
85
+ params = dict(params_obj) if params_obj else {}
86
+ else:
87
+ params = kwargs
88
+ # Execute via MCP tool's execute_mcp_tool method
89
+ action = f"{srv_name}_{t_name}"
90
+ return await mcp_ref.execute_mcp_tool(
91
+ action, params, execution_context=execution_context
92
+ )
93
+ return executor
94
+
95
+ executor_func = make_executor(server_name, tool_meta.name, mcp_tool)
96
+
97
+ # Store metadata for routing
98
+ executor_func._mcp_server = server_name
99
+ executor_func._mcp_tool = tool_meta.name
100
+
101
+ # Register with the registry
102
+ self.registry.wrap_function(
103
+ name=action_name,
104
+ function=executor_func,
105
+ description=tool_meta.description or f"Execute {tool_meta.name} on {server_name}",
106
+ tool='mcp',
107
+ param_model=param_model
108
+ )
109
+
110
+ registered_count += 1
111
+ self.logger.debug(f"Registered MCP action: {action_name}")
112
+
113
+ except Exception as e:
114
+ self.logger.warning(f"Failed to register MCP tool {server_name}_{tool_meta.name}: {e}")
115
+
116
+ self.logger.info(f"✅ Registered {registered_count} MCP tools as direct actions")
117
+
118
+ except Exception as e:
119
+ self.logger.error(f"Failed to register MCP tools as direct actions: {e}", exc_info=True)
120
+
121
+ def create_param_model(self, action_name: str, schema: Dict[str, Any]) -> Type[BaseModel]:
122
+ """Create a dynamic Pydantic model from a JSON schema.
123
+
124
+ Args:
125
+ action_name: Name for the model
126
+ schema: JSON schema dict with properties, required, etc.
127
+
128
+ Returns:
129
+ Dynamically created Pydantic BaseModel subclass
130
+ """
131
+ from pydantic import create_model, Field
132
+ from typing import Optional, List, Any as TypingAny
133
+
134
+ properties = schema.get('properties', {})
135
+ required = set(schema.get('required', []))
136
+
137
+ # Build field definitions
138
+ fields = {}
139
+ for prop_name, prop_schema in properties.items():
140
+ prop_type = prop_schema.get('type', 'string')
141
+ description = prop_schema.get('description', '')
142
+ default = prop_schema.get('default', ...)
143
+
144
+ # Map JSON schema types to Python types
145
+ type_mapping = {
146
+ 'string': str,
147
+ 'integer': int,
148
+ 'number': float,
149
+ 'boolean': bool,
150
+ 'array': list,
151
+ 'object': dict,
152
+ }
153
+
154
+ python_type = type_mapping.get(prop_type, TypingAny)
155
+
156
+ # WS-B5: preserve array item type (List[item]) instead of bare list.
157
+ # Objects stay as dict (lenient) so previously-valid payloads with
158
+ # unforeseen keys are never rejected.
159
+ if prop_type == 'array':
160
+ items_schema = prop_schema.get('items') or {}
161
+ item_type = type_mapping.get(items_schema.get('type'), TypingAny)
162
+ python_type = List[item_type]
163
+
164
+ # Handle nullable types (anyOf with null)
165
+ if 'anyOf' in prop_schema:
166
+ for option in prop_schema['anyOf']:
167
+ if option.get('type') == 'null':
168
+ python_type = Optional[python_type]
169
+ break
170
+
171
+ # Set default based on whether field is required
172
+ if prop_name in required:
173
+ field_default = ... # Required
174
+ elif default != ...:
175
+ field_default = default
176
+ else:
177
+ field_default = None
178
+ python_type = Optional[python_type] if python_type != Optional else python_type
179
+
180
+ # Create field with description
181
+ fields[prop_name] = (python_type, Field(default=field_default, description=description))
182
+
183
+ # Create dynamic model
184
+ model_name = f"MCP_{action_name.replace('-', '_').title().replace('_', '')}Params"
185
+
186
+ try:
187
+ return create_model(model_name, **fields)
188
+ except Exception as e:
189
+ self.logger.warning(f"Failed to create param model for {action_name}: {e}, using empty model")
190
+ return create_model(model_name)
@@ -0,0 +1,31 @@
1
+ """Pure-ish send helper for the `message` action: resolve tier, gate, route.
2
+ Router is any object with async send_message(chat_id, text, surface_id)."""
3
+ import logging
4
+ from core.surfaces.outbound_target import resolve_target_tier
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ async def perform_message_send(*, router, allowlist, owner_targets, user_id,
10
+ surface, target, text, action="send", reply_to=None,
11
+ message_id=None) -> dict:
12
+ tier = resolve_target_tier(surface=surface, target=target, user_id=user_id,
13
+ allowlist=allowlist, owner_targets=owner_targets)
14
+ if tier == "denied":
15
+ return {"success": False, "tier": "denied", "surface": surface, "target": target,
16
+ "error": ("target not on owner allowlist; ask the owner to run "
17
+ f"`/allow {surface} {target}` (or `polyrob owner allow {surface} {target}`)")}
18
+ if action not in ("send", "reply"):
19
+ # edit/delete/react are capability-gated and deferred to P2; fail cleanly.
20
+ return {"success": False, "tier": tier, "surface": surface, "target": target,
21
+ "error": f"action '{action}' not supported yet on {surface}"}
22
+ if router is None:
23
+ return {"success": False, "tier": tier, "surface": surface, "target": target,
24
+ "error": "no message_router available (SINGULAR_CHAT_ENABLED off?)"}
25
+ try:
26
+ ok = await router.send_message(chat_id=target, text=text, surface_id=surface)
27
+ except Exception as e: # fail-open: never crash the loop on a send fault
28
+ logger.error("message send failed: %s", e, exc_info=True)
29
+ return {"success": False, "tier": tier, "surface": surface, "target": target, "error": str(e)}
30
+ return {"success": bool(ok), "tier": tier, "surface": surface, "target": target,
31
+ "error": None if ok else "send returned false"}
@@ -0,0 +1,4 @@
1
+ from tools.controller.registry.service import Registry
2
+ from tools.controller.registry.views import ActionModel, ActionRegistry, RegisteredAction
3
+
4
+ __all__ = ['Registry', 'ActionModel', 'ActionRegistry', 'RegisteredAction']
@@ -0,0 +1,395 @@
1
+ """Provider-specific tool schema generators for native tool calling"""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Dict, List, Optional, Type, Tuple
5
+ import json
6
+ import logging
7
+ import os
8
+ from pydantic import BaseModel
9
+
10
+ from tools.controller.registry.views import RegisteredAction
11
+ from agents.task.utils import fix_openai_schema, fix_anthropic_schema
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class SchemaValidationError(Exception):
17
+ """Raised when a generated schema fails validation."""
18
+ pass
19
+
20
+
21
+ _VALID_SCHEMA_ERROR_POLICIES = ("DROP_TOOL", "RAISE", "WARN")
22
+
23
+
24
+ def get_schema_error_policy() -> str:
25
+ """Policy for handling a tool whose generated native schema fails validation.
26
+
27
+ Env ``TOOL_SCHEMA_ERROR_POLICY`` (default ``DROP_TOOL``):
28
+ - ``DROP_TOOL`` — exclude the offending tool from the emitted list (never
29
+ ship an invalid schema to the provider).
30
+ - ``RAISE`` — propagate ``SchemaValidationError``.
31
+ - ``WARN`` — legacy: log and ship the tool anyway.
32
+ """
33
+ val = os.getenv("TOOL_SCHEMA_ERROR_POLICY", "DROP_TOOL").upper().strip()
34
+ return val if val in _VALID_SCHEMA_ERROR_POLICIES else "DROP_TOOL"
35
+
36
+
37
+ class ToolSchemaGenerator(ABC):
38
+ """Base class for provider-specific tool schema generation"""
39
+
40
+ @abstractmethod
41
+ def generate_tool_schema(self, action: RegisteredAction) -> Dict[str, Any]:
42
+ """Generate tool schema for specific provider"""
43
+ pass
44
+
45
+ @abstractmethod
46
+ def generate_tools_list(self, actions: List[RegisteredAction]) -> Any:
47
+ """Generate the complete tools list/object for the provider"""
48
+ pass
49
+
50
+ def _get_json_schema(self, param_model: Type[BaseModel]) -> Dict[str, Any]:
51
+ """Get JSON schema from Pydantic model, removing unnecessary fields"""
52
+ schema = param_model.model_json_schema()
53
+ # Remove title from properties to reduce token usage
54
+ if 'properties' in schema:
55
+ for prop in schema['properties'].values():
56
+ if isinstance(prop, dict) and 'title' in prop:
57
+ del prop['title']
58
+ # Remove title from root
59
+ if 'title' in schema:
60
+ del schema['title']
61
+ return schema
62
+
63
+ def _validate_schema(self, schema: Dict[str, Any], action_name: str) -> Tuple[bool, List[str]]:
64
+ """Validate a generated schema for common issues.
65
+
66
+ Returns:
67
+ Tuple of (is_valid, list of warning/error messages)
68
+ """
69
+ issues = []
70
+
71
+ # Check for basic structure
72
+ if not isinstance(schema, dict):
73
+ issues.append(f"Schema for '{action_name}' is not a dictionary")
74
+ return False, issues
75
+
76
+ # Check for required fields based on schema type
77
+ if 'function' in schema:
78
+ # OpenAI format
79
+ func = schema.get('function', {})
80
+ if not func.get('name'):
81
+ issues.append(f"OpenAI schema for '{action_name}' missing function name")
82
+ if not func.get('description'):
83
+ issues.append(f"OpenAI schema for '{action_name}' missing description")
84
+ if 'parameters' not in func:
85
+ issues.append(f"OpenAI schema for '{action_name}' missing parameters")
86
+ else:
87
+ params = func.get('parameters', {})
88
+ if params.get('type') != 'object':
89
+ issues.append(f"OpenAI schema for '{action_name}' parameters should have type 'object'")
90
+ elif 'input_schema' in schema:
91
+ # Anthropic format
92
+ if not schema.get('name'):
93
+ issues.append(f"Anthropic schema for '{action_name}' missing name")
94
+ if not schema.get('description'):
95
+ issues.append(f"Anthropic schema for '{action_name}' missing description")
96
+ input_schema = schema.get('input_schema', {})
97
+ if input_schema.get('type') != 'object':
98
+ issues.append(f"Anthropic schema for '{action_name}' input_schema should have type 'object'")
99
+ elif 'parameters' in schema and 'name' in schema:
100
+ # Gemini format
101
+ if not schema.get('name'):
102
+ issues.append(f"Gemini schema for '{action_name}' missing name")
103
+ if not schema.get('description'):
104
+ issues.append(f"Gemini schema for '{action_name}' missing description")
105
+
106
+ # Check for empty properties (debug only - not an error, some actions have no params)
107
+ properties = None
108
+ if 'function' in schema:
109
+ properties = schema.get('function', {}).get('parameters', {}).get('properties')
110
+ elif 'input_schema' in schema:
111
+ properties = schema.get('input_schema', {}).get('properties')
112
+ elif 'parameters' in schema:
113
+ properties = schema.get('parameters', {}).get('properties')
114
+
115
+ # Note: Empty properties is not an error - some actions intentionally have no parameters
116
+ # (e.g., get_positions, get_portfolio_value, etc.)
117
+ # Only log at debug level to avoid noise
118
+ if properties is not None and len(properties) == 0:
119
+ logger.debug(f"Schema for '{action_name}' has empty properties (no parameters) - this is expected for parameterless actions")
120
+
121
+ is_valid = len([i for i in issues if 'missing' in i.lower()]) == 0
122
+ return is_valid, issues
123
+
124
+ def validate_and_log(self, schema: Dict[str, Any], action_name: str) -> Dict[str, Any]:
125
+ """Validate schema and log any issues.
126
+
127
+ Args:
128
+ schema: The generated schema
129
+ action_name: Name of the action for logging
130
+
131
+ Returns:
132
+ The schema (unchanged)
133
+
134
+ Raises:
135
+ SchemaValidationError: If validation fails critically
136
+ """
137
+ is_valid, issues = self._validate_schema(schema, action_name)
138
+
139
+ if issues:
140
+ for issue in issues:
141
+ if 'missing' in issue.lower():
142
+ logger.error(f"Schema validation error: {issue}")
143
+ else:
144
+ logger.warning(f"Schema validation warning: {issue}")
145
+
146
+ if not is_valid:
147
+ raise SchemaValidationError(f"Schema validation failed for '{action_name}': {issues}")
148
+
149
+ return schema
150
+
151
+ def _apply_schema_error_policy(self, schema: Dict[str, Any], action_name: str) -> Optional[Dict[str, Any]]:
152
+ """Validate ``schema`` and apply ``TOOL_SCHEMA_ERROR_POLICY``.
153
+
154
+ Returns the schema if valid (or under WARN policy), ``None`` if the tool
155
+ should be dropped (DROP_TOOL), or raises ``SchemaValidationError`` (RAISE).
156
+ """
157
+ is_valid, issues = self._validate_schema(schema, action_name)
158
+ if is_valid:
159
+ return schema
160
+
161
+ policy = get_schema_error_policy()
162
+ msg = f"Schema validation failed for '{action_name}': {issues}"
163
+ if policy == "RAISE":
164
+ raise SchemaValidationError(msg)
165
+ if policy == "WARN":
166
+ logger.warning(f"{msg} (WARN policy: shipping tool anyway)")
167
+ return schema
168
+ # DROP_TOOL (default): never ship an invalid schema to the provider
169
+ logger.warning(f"{msg} (DROP_TOOL policy: excluding this tool from the list)")
170
+ return None
171
+
172
+
173
+ class OpenAISchemaGenerator(ToolSchemaGenerator):
174
+ """OpenAI function calling schema generator"""
175
+
176
+ def generate_tool_schema(self, action: RegisteredAction, validate: bool = True) -> Dict[str, Any]:
177
+ """Generate OpenAI function schema"""
178
+ # Get the base schema
179
+ params_schema = self._get_json_schema(action.param_model)
180
+
181
+ # Apply OpenAI-specific fixes (additionalProperties: false)
182
+ params_schema = fix_openai_schema(params_schema)
183
+
184
+ schema = {
185
+ "type": "function",
186
+ "function": {
187
+ "name": action.name,
188
+ "description": action.description,
189
+ "parameters": params_schema
190
+ }
191
+ }
192
+
193
+ # Validate if requested (log warnings but don't fail)
194
+ if validate:
195
+ try:
196
+ self.validate_and_log(schema, action.name)
197
+ except SchemaValidationError as e:
198
+ logger.warning(f"Schema validation issue (continuing anyway): {e}")
199
+
200
+ return schema
201
+
202
+ def generate_tools_list(self, actions: List[RegisteredAction]) -> List[Dict[str, Any]]:
203
+ """Generate OpenAI tools list with deduplication by name + error policy."""
204
+ seen_names = set()
205
+ tools = []
206
+ for action in actions:
207
+ if action.name in seen_names:
208
+ logger.debug(f"Skipping duplicate tool name: {action.name}")
209
+ continue
210
+ seen_names.add(action.name)
211
+ schema = self._apply_schema_error_policy(
212
+ self.generate_tool_schema(action, validate=False), action.name
213
+ )
214
+ if schema is not None:
215
+ tools.append(schema)
216
+ return tools
217
+
218
+
219
+ class AnthropicSchemaGenerator(ToolSchemaGenerator):
220
+ """Anthropic tool calling schema generator"""
221
+
222
+ def generate_tool_schema(self, action: RegisteredAction, validate: bool = True) -> Dict[str, Any]:
223
+ """Generate Anthropic tool schema"""
224
+ # Get the base schema
225
+ params_schema = self._get_json_schema(action.param_model)
226
+
227
+ # Apply Anthropic-specific fixes (additionalProperties: false)
228
+ params_schema = fix_anthropic_schema(params_schema)
229
+
230
+ schema = {
231
+ "name": action.name,
232
+ "description": action.description,
233
+ "input_schema": params_schema
234
+ }
235
+
236
+ # Validate if requested (log warnings but don't fail)
237
+ if validate:
238
+ try:
239
+ self.validate_and_log(schema, action.name)
240
+ except SchemaValidationError as e:
241
+ logger.warning(f"Schema validation issue (continuing anyway): {e}")
242
+
243
+ return schema
244
+
245
+ def generate_tools_list(self, actions: List[RegisteredAction]) -> List[Dict[str, Any]]:
246
+ """Generate Anthropic tools list with deduplication by name + error policy."""
247
+ seen_names = set()
248
+ tools = []
249
+ for action in actions:
250
+ if action.name in seen_names:
251
+ logger.debug(f"Skipping duplicate tool name: {action.name}")
252
+ continue
253
+ seen_names.add(action.name)
254
+ schema = self._apply_schema_error_policy(
255
+ self.generate_tool_schema(action, validate=False), action.name
256
+ )
257
+ if schema is not None:
258
+ tools.append(schema)
259
+ return tools
260
+
261
+
262
+ class GeminiSchemaGenerator(ToolSchemaGenerator):
263
+ """Google Gemini function calling schema generator"""
264
+
265
+ def generate_tool_schema(self, action: RegisteredAction, validate: bool = True) -> Dict[str, Any]:
266
+ """Generate Gemini function schema"""
267
+ base_schema = self._get_json_schema(action.param_model)
268
+
269
+ # Gemini expects 'type' and 'properties' at minimum
270
+ parameters = {
271
+ "type": "object",
272
+ "properties": base_schema.get("properties", {}),
273
+ }
274
+
275
+ # Only add required if it exists and is non-empty
276
+ if "required" in base_schema and base_schema["required"]:
277
+ parameters["required"] = base_schema["required"]
278
+
279
+ schema = {
280
+ "name": action.name,
281
+ "description": action.description,
282
+ "parameters": parameters
283
+ }
284
+
285
+ # Validate if requested (log warnings but don't fail)
286
+ if validate:
287
+ try:
288
+ self.validate_and_log(schema, action.name)
289
+ except SchemaValidationError as e:
290
+ logger.warning(f"Schema validation issue (continuing anyway): {e}")
291
+
292
+ return schema
293
+
294
+ def generate_tools_list(self, actions: List[RegisteredAction]) -> List[Dict[str, Any]]:
295
+ """Generate Gemini tools list with function declarations + error policy."""
296
+ if not actions:
297
+ return []
298
+
299
+ declarations = []
300
+ for action in actions:
301
+ schema = self._apply_schema_error_policy(
302
+ self.generate_tool_schema(action, validate=False), action.name
303
+ )
304
+ if schema is not None:
305
+ declarations.append(schema)
306
+ if not declarations:
307
+ return []
308
+ return [{"function_declarations": declarations}]
309
+
310
+
311
+ class JSONFallbackSchemaGenerator(ToolSchemaGenerator):
312
+ """JSON schema generator for providers without native tool support"""
313
+
314
+ def generate_tool_schema(self, action: RegisteredAction) -> Dict[str, Any]:
315
+ """Generate JSON schema for action"""
316
+ return {
317
+ "name": action.name,
318
+ "description": action.description,
319
+ "parameters": self._get_json_schema(action.param_model)
320
+ }
321
+
322
+ def generate_tools_list(self, actions: List[RegisteredAction]) -> Dict[str, Any]:
323
+ """Generate JSON schema for all actions"""
324
+ # This creates a schema that expects: {"action": [{"action_name": {...params}}]}
325
+ action_properties = {}
326
+
327
+ for action in actions:
328
+ action_properties[action.name] = {
329
+ "type": "object",
330
+ "description": action.description,
331
+ "properties": action.param_model.model_json_schema().get("properties", {}),
332
+ "required": action.param_model.model_json_schema().get("required", [])
333
+ }
334
+
335
+ return {
336
+ "type": "object",
337
+ "properties": {
338
+ "action": {
339
+ "type": "array",
340
+ "description": "List of actions to execute",
341
+ "items": {
342
+ "type": "object",
343
+ "properties": action_properties,
344
+ "additionalProperties": False
345
+ }
346
+ }
347
+ },
348
+ "required": ["action"]
349
+ }
350
+
351
+
352
+ # Provider registry mapping
353
+ # NOTE: DeepSeek V3+ uses OpenAI-compatible function calling API
354
+ # NOTE: OpenRouter proxies to many providers and uses OpenAI-compatible format
355
+ SCHEMA_GENERATORS = {
356
+ "openai": OpenAISchemaGenerator(),
357
+ "anthropic": AnthropicSchemaGenerator(),
358
+ "gemini": GeminiSchemaGenerator(),
359
+ "google": GeminiSchemaGenerator(), # Alias
360
+ "deepseek": OpenAISchemaGenerator(), # DeepSeek V3+ is OpenAI-compatible
361
+ "openrouter": OpenAISchemaGenerator(), # OpenRouter uses OpenAI-compatible format
362
+ "nvidia": OpenAISchemaGenerator(), # NVIDIA NIM uses OpenAI-compatible format
363
+ "groq": JSONFallbackSchemaGenerator(),
364
+ "fireworks": JSONFallbackSchemaGenerator(),
365
+ "default": JSONFallbackSchemaGenerator(),
366
+ }
367
+
368
+
369
+ # Providers that have intentionally fallen back to the JSON generator are tracked
370
+ # so we warn exactly once per unrecognized provider string (avoids log spam).
371
+ _warned_fallback_providers: set = set()
372
+
373
+
374
+ def get_schema_generator(provider: str) -> ToolSchemaGenerator:
375
+ """Get the appropriate schema generator for a provider.
376
+
377
+ Falls back to the JSON generator for unknown providers — but warns once so a
378
+ misspelled/new provider silently losing native tool-calling is visible
379
+ (see the new-provider checklist: SCHEMA_GENERATORS is the easy-to-miss seam).
380
+ """
381
+ provider_lower = provider.lower()
382
+
383
+ # Check for partial matches (e.g., "openai-gpt4" -> "openai")
384
+ for key in SCHEMA_GENERATORS:
385
+ if key != "default" and key in provider_lower:
386
+ return SCHEMA_GENERATORS[key]
387
+
388
+ if provider_lower not in _warned_fallback_providers:
389
+ _warned_fallback_providers.add(provider_lower)
390
+ logger.warning(
391
+ f"No native schema generator matched provider '{provider}' — using JSON "
392
+ f"fallback (native tool-calling disabled). If this provider supports native "
393
+ f"tools, add it to SCHEMA_GENERATORS."
394
+ )
395
+ return SCHEMA_GENERATORS["default"]