coco-framework 0.1.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 (400) hide show
  1. coco/__init__.py +3 -0
  2. coco/agents/__init__.py +9 -0
  3. coco/agents/_subagent_transformer.py +307 -0
  4. coco/agents/factory.py +2164 -0
  5. coco/agents/middleware/__init__.py +100 -0
  6. coco/agents/middleware/_execution.py +385 -0
  7. coco/agents/middleware/_redaction.py +454 -0
  8. coco/agents/middleware/_retry.py +134 -0
  9. coco/agents/middleware/_trace_policy.py +61 -0
  10. coco/agents/middleware/context_editing.py +306 -0
  11. coco/agents/middleware/file_search.py +433 -0
  12. coco/agents/middleware/human_in_the_loop.py +680 -0
  13. coco/agents/middleware/internal_call_transformer.py +108 -0
  14. coco/agents/middleware/model_call_limit.py +267 -0
  15. coco/agents/middleware/model_fallback.py +417 -0
  16. coco/agents/middleware/model_retry.py +327 -0
  17. coco/agents/middleware/pii.py +878 -0
  18. coco/agents/middleware/provider_tool_search.py +307 -0
  19. coco/agents/middleware/shell_tool.py +949 -0
  20. coco/agents/middleware/summarization.py +910 -0
  21. coco/agents/middleware/todo.py +357 -0
  22. coco/agents/middleware/tool_call_limit.py +495 -0
  23. coco/agents/middleware/tool_emulator.py +251 -0
  24. coco/agents/middleware/tool_error.py +209 -0
  25. coco/agents/middleware/tool_retry.py +421 -0
  26. coco/agents/middleware/tool_selection.py +515 -0
  27. coco/agents/middleware/types.py +2201 -0
  28. coco/agents/structured_output.py +472 -0
  29. coco/chat_models/__init__.py +7 -0
  30. coco/chat_models/base.py +1082 -0
  31. coco/embeddings/__init__.py +18 -0
  32. coco/embeddings/base.py +279 -0
  33. coco/mcp/__init__.py +36 -0
  34. coco/mcp/adapter.py +245 -0
  35. coco/mcp/elicitation.py +394 -0
  36. coco/mcp/tools.py +307 -0
  37. coco/messages/__init__.py +73 -0
  38. coco/py.typed +0 -0
  39. coco/rate_limiters/__init__.py +13 -0
  40. coco/tools/__init__.py +22 -0
  41. coco/tools/tool_node.py +20 -0
  42. coco_anthropic/__init__.py +15 -0
  43. coco_anthropic/_client_utils.py +81 -0
  44. coco_anthropic/_compat.py +228 -0
  45. coco_anthropic/_sdk_compat.py +124 -0
  46. coco_anthropic/_version.py +3 -0
  47. coco_anthropic/chat_models.py +3349 -0
  48. coco_anthropic/data/__init__.py +1 -0
  49. coco_anthropic/data/_profiles.py +491 -0
  50. coco_anthropic/data/profile_augmentations.toml +69 -0
  51. coco_anthropic/experimental.py +140 -0
  52. coco_anthropic/llms.py +428 -0
  53. coco_anthropic/middleware/__init__.py +25 -0
  54. coco_anthropic/middleware/anthropic_tools.py +1197 -0
  55. coco_anthropic/middleware/bash.py +84 -0
  56. coco_anthropic/middleware/file_search.py +360 -0
  57. coco_anthropic/middleware/prompt_caching.py +276 -0
  58. coco_anthropic/output_parsers.py +93 -0
  59. coco_anthropic/py.typed +0 -0
  60. coco_core/__init__.py +20 -0
  61. coco_core/_api/__init__.py +87 -0
  62. coco_core/_api/beta_decorator.py +266 -0
  63. coco_core/_api/deprecation.py +637 -0
  64. coco_core/_api/internal.py +23 -0
  65. coco_core/_api/path.py +50 -0
  66. coco_core/_import_utils.py +41 -0
  67. coco_core/_protocol.py +625 -0
  68. coco_core/_security/__init__.py +36 -0
  69. coco_core/_security/_exceptions.py +9 -0
  70. coco_core/_security/_policy.py +313 -0
  71. coco_core/_security/_ssrf_protection.py +155 -0
  72. coco_core/_security/_transport.py +254 -0
  73. coco_core/agents.py +258 -0
  74. coco_core/caches.py +272 -0
  75. coco_core/callbacks/__init__.py +132 -0
  76. coco_core/callbacks/base.py +1229 -0
  77. coco_core/callbacks/file.py +266 -0
  78. coco_core/callbacks/manager.py +2826 -0
  79. coco_core/callbacks/stdout.py +123 -0
  80. coco_core/callbacks/streaming_stdout.py +154 -0
  81. coco_core/callbacks/usage.py +124 -0
  82. coco_core/chat_history.py +267 -0
  83. coco_core/chat_loaders.py +26 -0
  84. coco_core/chat_sessions.py +19 -0
  85. coco_core/cross_encoders.py +18 -0
  86. coco_core/document_loaders/__init__.py +36 -0
  87. coco_core/document_loaders/base.py +155 -0
  88. coco_core/document_loaders/blob_loaders.py +38 -0
  89. coco_core/documents/__init__.py +55 -0
  90. coco_core/documents/base.py +347 -0
  91. coco_core/documents/compressor.py +74 -0
  92. coco_core/documents/transformers.py +79 -0
  93. coco_core/embeddings/__init__.py +31 -0
  94. coco_core/embeddings/embeddings.py +78 -0
  95. coco_core/embeddings/fake.py +129 -0
  96. coco_core/env.py +22 -0
  97. coco_core/example_selectors/__init__.py +47 -0
  98. coco_core/example_selectors/base.py +60 -0
  99. coco_core/example_selectors/length_based.py +131 -0
  100. coco_core/example_selectors/semantic_similarity.py +366 -0
  101. coco_core/exceptions.py +166 -0
  102. coco_core/globals.py +72 -0
  103. coco_core/indexing/__init__.py +53 -0
  104. coco_core/indexing/api.py +954 -0
  105. coco_core/indexing/base.py +661 -0
  106. coco_core/indexing/in_memory.py +104 -0
  107. coco_core/language_models/__init__.py +116 -0
  108. coco_core/language_models/_compat_bridge.py +844 -0
  109. coco_core/language_models/_utils.py +343 -0
  110. coco_core/language_models/base.py +500 -0
  111. coco_core/language_models/chat_model_stream.py +1525 -0
  112. coco_core/language_models/chat_models.py +2748 -0
  113. coco_core/language_models/fake.py +137 -0
  114. coco_core/language_models/fake_chat_models.py +396 -0
  115. coco_core/language_models/llms.py +1569 -0
  116. coco_core/language_models/model_profile.py +183 -0
  117. coco_core/load/__init__.py +44 -0
  118. coco_core/load/_validation.py +191 -0
  119. coco_core/load/dump.py +120 -0
  120. coco_core/load/load.py +828 -0
  121. coco_core/load/mapping.py +1097 -0
  122. coco_core/load/serializable.py +406 -0
  123. coco_core/messages/__init__.py +198 -0
  124. coco_core/messages/ai.py +854 -0
  125. coco_core/messages/base.py +518 -0
  126. coco_core/messages/block_translators/__init__.py +112 -0
  127. coco_core/messages/block_translators/anthropic.py +534 -0
  128. coco_core/messages/block_translators/bedrock.py +108 -0
  129. coco_core/messages/block_translators/bedrock_converse.py +329 -0
  130. coco_core/messages/block_translators/coco_v0.py +303 -0
  131. coco_core/messages/block_translators/google_genai.py +610 -0
  132. coco_core/messages/block_translators/google_vertexai.py +21 -0
  133. coco_core/messages/block_translators/groq.py +157 -0
  134. coco_core/messages/block_translators/openai.py +1089 -0
  135. coco_core/messages/chat.py +64 -0
  136. coco_core/messages/content.py +1488 -0
  137. coco_core/messages/function.py +62 -0
  138. coco_core/messages/human.py +70 -0
  139. coco_core/messages/modifier.py +33 -0
  140. coco_core/messages/system.py +70 -0
  141. coco_core/messages/tool.py +418 -0
  142. coco_core/messages/utils.py +2442 -0
  143. coco_core/output_parsers/__init__.py +101 -0
  144. coco_core/output_parsers/base.py +361 -0
  145. coco_core/output_parsers/format_instructions.py +16 -0
  146. coco_core/output_parsers/json.py +139 -0
  147. coco_core/output_parsers/list.py +249 -0
  148. coco_core/output_parsers/openai_functions.py +324 -0
  149. coco_core/output_parsers/openai_tools.py +383 -0
  150. coco_core/output_parsers/pydantic.py +141 -0
  151. coco_core/output_parsers/string.py +63 -0
  152. coco_core/output_parsers/transform.py +175 -0
  153. coco_core/output_parsers/xml.py +301 -0
  154. coco_core/outputs/__init__.py +66 -0
  155. coco_core/outputs/chat_generation.py +147 -0
  156. coco_core/outputs/chat_result.py +39 -0
  157. coco_core/outputs/generation.py +80 -0
  158. coco_core/outputs/llm_result.py +112 -0
  159. coco_core/outputs/run_info.py +22 -0
  160. coco_core/prompt_values.py +161 -0
  161. coco_core/prompts/__init__.py +101 -0
  162. coco_core/prompts/base.py +508 -0
  163. coco_core/prompts/chat.py +1495 -0
  164. coco_core/prompts/dict.py +177 -0
  165. coco_core/prompts/few_shot.py +483 -0
  166. coco_core/prompts/few_shot_with_templates.py +239 -0
  167. coco_core/prompts/image.py +177 -0
  168. coco_core/prompts/loading.py +296 -0
  169. coco_core/prompts/message.py +97 -0
  170. coco_core/prompts/prompt.py +312 -0
  171. coco_core/prompts/string.py +399 -0
  172. coco_core/prompts/structured.py +214 -0
  173. coco_core/py.typed +0 -0
  174. coco_core/rate_limiters.py +254 -0
  175. coco_core/retrievers.py +328 -0
  176. coco_core/runnables/__init__.py +136 -0
  177. coco_core/runnables/base.py +6725 -0
  178. coco_core/runnables/branch.py +457 -0
  179. coco_core/runnables/config.py +712 -0
  180. coco_core/runnables/configurable.py +710 -0
  181. coco_core/runnables/fallbacks.py +663 -0
  182. coco_core/runnables/graph.py +741 -0
  183. coco_core/runnables/graph_ascii.py +366 -0
  184. coco_core/runnables/graph_mermaid.py +503 -0
  185. coco_core/runnables/graph_png.py +215 -0
  186. coco_core/runnables/history.py +631 -0
  187. coco_core/runnables/passthrough.py +849 -0
  188. coco_core/runnables/retry.py +379 -0
  189. coco_core/runnables/router.py +239 -0
  190. coco_core/runnables/schema.py +188 -0
  191. coco_core/runnables/utils.py +798 -0
  192. coco_core/stores.py +291 -0
  193. coco_core/structured_query.py +203 -0
  194. coco_core/sys_info.py +154 -0
  195. coco_core/tools/__init__.py +95 -0
  196. coco_core/tools/base.py +1948 -0
  197. coco_core/tools/convert.py +497 -0
  198. coco_core/tools/render.py +67 -0
  199. coco_core/tools/retriever.py +94 -0
  200. coco_core/tools/simple.py +208 -0
  201. coco_core/tools/structured.py +322 -0
  202. coco_core/tracers/__init__.py +47 -0
  203. coco_core/tracers/_compat.py +95 -0
  204. coco_core/tracers/_context.py +337 -0
  205. coco_core/tracers/_streaming.py +53 -0
  206. coco_core/tracers/base.py +955 -0
  207. coco_core/tracers/coco.py +279 -0
  208. coco_core/tracers/context.py +205 -0
  209. coco_core/tracers/core.py +766 -0
  210. coco_core/tracers/event_stream.py +1101 -0
  211. coco_core/tracers/log_stream.py +765 -0
  212. coco_core/tracers/memory_stream.py +200 -0
  213. coco_core/tracers/root_listeners.py +130 -0
  214. coco_core/tracers/run_collector.py +40 -0
  215. coco_core/tracers/schemas.py +14 -0
  216. coco_core/tracers/stdout.py +205 -0
  217. coco_core/utils/__init__.py +111 -0
  218. coco_core/utils/_gateway.py +274 -0
  219. coco_core/utils/_merge.py +218 -0
  220. coco_core/utils/aiter.py +357 -0
  221. coco_core/utils/env.py +86 -0
  222. coco_core/utils/formatting.py +80 -0
  223. coco_core/utils/function_calling.py +869 -0
  224. coco_core/utils/html.py +132 -0
  225. coco_core/utils/image.py +15 -0
  226. coco_core/utils/input.py +82 -0
  227. coco_core/utils/interactive_env.py +12 -0
  228. coco_core/utils/iter.py +229 -0
  229. coco_core/utils/json.py +230 -0
  230. coco_core/utils/json_schema.py +273 -0
  231. coco_core/utils/mustache.py +706 -0
  232. coco_core/utils/pydantic.py +631 -0
  233. coco_core/utils/strings.py +69 -0
  234. coco_core/utils/usage.py +61 -0
  235. coco_core/utils/utils.py +528 -0
  236. coco_core/utils/uuid.py +57 -0
  237. coco_core/vectorstores/__init__.py +53 -0
  238. coco_core/vectorstores/base.py +1111 -0
  239. coco_core/vectorstores/in_memory.py +546 -0
  240. coco_core/vectorstores/utils.py +163 -0
  241. coco_core/version.py +3 -0
  242. coco_framework-0.1.0.dist-info/METADATA +184 -0
  243. coco_framework-0.1.0.dist-info/RECORD +400 -0
  244. coco_framework-0.1.0.dist-info/WHEEL +4 -0
  245. coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
  246. coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
  247. coco_graph/_internal/__init__.py +4 -0
  248. coco_graph/_internal/_cache.py +31 -0
  249. coco_graph/_internal/_config.py +474 -0
  250. coco_graph/_internal/_constants.py +140 -0
  251. coco_graph/_internal/_fields.py +213 -0
  252. coco_graph/_internal/_future.py +220 -0
  253. coco_graph/_internal/_pydantic.py +275 -0
  254. coco_graph/_internal/_queue.py +124 -0
  255. coco_graph/_internal/_replay.py +90 -0
  256. coco_graph/_internal/_retry.py +29 -0
  257. coco_graph/_internal/_runnable.py +969 -0
  258. coco_graph/_internal/_scratchpad.py +19 -0
  259. coco_graph/_internal/_serde.py +253 -0
  260. coco_graph/_internal/_timeout.py +25 -0
  261. coco_graph/_internal/_typing.py +54 -0
  262. coco_graph/cache/base/__init__.py +48 -0
  263. coco_graph/cache/base/py.typed +0 -0
  264. coco_graph/cache/memory/__init__.py +73 -0
  265. coco_graph/cache/redis/__init__.py +144 -0
  266. coco_graph/cache/sqlite/__init__.py +120 -0
  267. coco_graph/callbacks.py +394 -0
  268. coco_graph/channels/__init__.py +29 -0
  269. coco_graph/channels/any_value.py +72 -0
  270. coco_graph/channels/base.py +121 -0
  271. coco_graph/channels/binop.py +155 -0
  272. coco_graph/channels/delta.py +202 -0
  273. coco_graph/channels/ephemeral_value.py +79 -0
  274. coco_graph/channels/last_value.py +151 -0
  275. coco_graph/channels/named_barrier_value.py +167 -0
  276. coco_graph/channels/topic.py +94 -0
  277. coco_graph/channels/untracked_value.py +73 -0
  278. coco_graph/checkpoint/base/__init__.py +861 -0
  279. coco_graph/checkpoint/base/id.py +109 -0
  280. coco_graph/checkpoint/base/py.typed +0 -0
  281. coco_graph/checkpoint/memory/__init__.py +698 -0
  282. coco_graph/checkpoint/memory/py.typed +0 -0
  283. coco_graph/checkpoint/postgres/__init__.py +602 -0
  284. coco_graph/checkpoint/postgres/_ainternal.py +23 -0
  285. coco_graph/checkpoint/postgres/_internal.py +21 -0
  286. coco_graph/checkpoint/postgres/aio.py +691 -0
  287. coco_graph/checkpoint/postgres/base.py +666 -0
  288. coco_graph/checkpoint/postgres/py.typed +0 -0
  289. coco_graph/checkpoint/postgres/shallow.py +967 -0
  290. coco_graph/checkpoint/serde/__init__.py +0 -0
  291. coco_graph/checkpoint/serde/_msgpack.py +97 -0
  292. coco_graph/checkpoint/serde/base.py +64 -0
  293. coco_graph/checkpoint/serde/encrypted.py +80 -0
  294. coco_graph/checkpoint/serde/event_hooks.py +52 -0
  295. coco_graph/checkpoint/serde/jsonplus.py +883 -0
  296. coco_graph/checkpoint/serde/py.typed +0 -0
  297. coco_graph/checkpoint/serde/types.py +68 -0
  298. coco_graph/checkpoint/sqlite/__init__.py +645 -0
  299. coco_graph/checkpoint/sqlite/_delta.py +172 -0
  300. coco_graph/checkpoint/sqlite/aio.py +738 -0
  301. coco_graph/checkpoint/sqlite/py.typed +0 -0
  302. coco_graph/checkpoint/sqlite/utils.py +116 -0
  303. coco_graph/config.py +196 -0
  304. coco_graph/constants.py +64 -0
  305. coco_graph/errors.py +241 -0
  306. coco_graph/func/__init__.py +620 -0
  307. coco_graph/graph/__init__.py +12 -0
  308. coco_graph/graph/_branch.py +225 -0
  309. coco_graph/graph/_node.py +103 -0
  310. coco_graph/graph/message.py +437 -0
  311. coco_graph/graph/state.py +1978 -0
  312. coco_graph/graph/ui.py +227 -0
  313. coco_graph/managed/__init__.py +3 -0
  314. coco_graph/managed/base.py +31 -0
  315. coco_graph/managed/is_last_step.py +24 -0
  316. coco_graph/prebuilt/__init__.py +23 -0
  317. coco_graph/prebuilt/_tool_call_stream.py +117 -0
  318. coco_graph/prebuilt/_tool_call_transformer.py +165 -0
  319. coco_graph/prebuilt/chat_agent_executor.py +1015 -0
  320. coco_graph/prebuilt/interrupt.py +105 -0
  321. coco_graph/prebuilt/py.typed +0 -0
  322. coco_graph/prebuilt/tool_node.py +2030 -0
  323. coco_graph/prebuilt/tool_validator.py +221 -0
  324. coco_graph/pregel/__init__.py +3 -0
  325. coco_graph/pregel/_algo.py +1460 -0
  326. coco_graph/pregel/_call.py +298 -0
  327. coco_graph/pregel/_checkpoint.py +331 -0
  328. coco_graph/pregel/_config.py +0 -0
  329. coco_graph/pregel/_draw.py +294 -0
  330. coco_graph/pregel/_executor.py +223 -0
  331. coco_graph/pregel/_io.py +174 -0
  332. coco_graph/pregel/_log.py +3 -0
  333. coco_graph/pregel/_loop.py +1988 -0
  334. coco_graph/pregel/_messages.py +461 -0
  335. coco_graph/pregel/_read.py +312 -0
  336. coco_graph/pregel/_retry.py +854 -0
  337. coco_graph/pregel/_runner.py +941 -0
  338. coco_graph/pregel/_tools.py +268 -0
  339. coco_graph/pregel/_utils.py +221 -0
  340. coco_graph/pregel/_validate.py +120 -0
  341. coco_graph/pregel/_write.py +192 -0
  342. coco_graph/pregel/debug.py +302 -0
  343. coco_graph/pregel/main.py +4364 -0
  344. coco_graph/pregel/protocol.py +288 -0
  345. coco_graph/pregel/types.py +38 -0
  346. coco_graph/py.typed +0 -0
  347. coco_graph/runtime.py +350 -0
  348. coco_graph/store/base/__init__.py +1322 -0
  349. coco_graph/store/base/batch.py +371 -0
  350. coco_graph/store/base/embed.py +436 -0
  351. coco_graph/store/base/py.typed +0 -0
  352. coco_graph/store/memory/__init__.py +592 -0
  353. coco_graph/store/memory/py.typed +0 -0
  354. coco_graph/store/postgres/__init__.py +4 -0
  355. coco_graph/store/postgres/aio.py +592 -0
  356. coco_graph/store/postgres/base.py +1480 -0
  357. coco_graph/store/postgres/py.typed +0 -0
  358. coco_graph/store/sqlite/__init__.py +4 -0
  359. coco_graph/store/sqlite/aio.py +632 -0
  360. coco_graph/store/sqlite/base.py +1514 -0
  361. coco_graph/stream/__init__.py +45 -0
  362. coco_graph/stream/_convert.py +32 -0
  363. coco_graph/stream/_mux.py +523 -0
  364. coco_graph/stream/_types.py +330 -0
  365. coco_graph/stream/run_stream.py +735 -0
  366. coco_graph/stream/stream_channel.py +341 -0
  367. coco_graph/stream/transformers.py +1039 -0
  368. coco_graph/types.py +1079 -0
  369. coco_graph/typing.py +48 -0
  370. coco_graph/utils/__init__.py +1 -0
  371. coco_graph/utils/config.py +4 -0
  372. coco_graph/utils/runnable.py +3 -0
  373. coco_graph/version.py +13 -0
  374. coco_graph/warnings.py +69 -0
  375. coco_openai/__init__.py +20 -0
  376. coco_openai/_compat.py +45 -0
  377. coco_openai/_version.py +3 -0
  378. coco_openai/chat_models/__init__.py +6 -0
  379. coco_openai/chat_models/_client_utils.py +684 -0
  380. coco_openai/chat_models/_compat.py +525 -0
  381. coco_openai/chat_models/azure.py +1227 -0
  382. coco_openai/chat_models/base.py +5654 -0
  383. coco_openai/chat_models/codex.py +557 -0
  384. coco_openai/chatgpt_oauth.py +1063 -0
  385. coco_openai/data/__init__.py +1 -0
  386. coco_openai/data/_profiles.py +1655 -0
  387. coco_openai/data/profile_augmentations.toml +127 -0
  388. coco_openai/embeddings/__init__.py +6 -0
  389. coco_openai/embeddings/azure.py +240 -0
  390. coco_openai/embeddings/base.py +820 -0
  391. coco_openai/llms/__init__.py +6 -0
  392. coco_openai/llms/azure.py +249 -0
  393. coco_openai/llms/base.py +919 -0
  394. coco_openai/middleware/__init__.py +11 -0
  395. coco_openai/middleware/openai_moderation.py +484 -0
  396. coco_openai/output_parsers/__init__.py +9 -0
  397. coco_openai/output_parsers/tools.py +9 -0
  398. coco_openai/py.typed +0 -0
  399. coco_openai/tools/__init__.py +5 -0
  400. coco_openai/tools/custom_tool.py +111 -0
@@ -0,0 +1,254 @@
1
+ """SSRF-safe httpx transport with DNS resolution and IP pinning."""
2
+
3
+ import asyncio
4
+ import socket
5
+
6
+ import httpx
7
+
8
+ from coco_core._security._exceptions import SSRFBlockedError
9
+ from coco_core._security._policy import (
10
+ DEFAULT_SSRF_POLICY,
11
+ SSRFPolicy,
12
+ _effective_allowed_hosts,
13
+ validate_resolved_ip,
14
+ validate_url_sync,
15
+ )
16
+
17
+ # Keys that AsyncHTTPTransport accepts (forwarded from factory kwargs).
18
+ _TRANSPORT_KWARGS = frozenset(
19
+ {
20
+ "verify",
21
+ "cert",
22
+ "trust_env",
23
+ "http1",
24
+ "http2",
25
+ "limits",
26
+ "retries",
27
+ }
28
+ )
29
+
30
+
31
+ class SSRFSafeTransport(httpx.AsyncBaseTransport):
32
+ """httpx async transport that validates DNS results against an SSRF policy.
33
+
34
+ For every outgoing request the transport:
35
+ 1. Checks the URL scheme against `policy.allowed_schemes`.
36
+ 2. Validates the hostname against blocked patterns.
37
+ 3. Resolves DNS and validates **all** returned IPs.
38
+ 4. Rewrites the request to connect to the first valid IP while
39
+ preserving the original `Host` header and TLS SNI hostname.
40
+
41
+ Redirects are re-validated on each hop because `follow_redirects`
42
+ is set on the *client*, causing `handle_async_request` to be called
43
+ again for each redirect target.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ policy: SSRFPolicy = DEFAULT_SSRF_POLICY,
49
+ **transport_kwargs: object,
50
+ ) -> None:
51
+ self._policy = policy
52
+ self._inner = httpx.AsyncHTTPTransport(**transport_kwargs) # type: ignore[arg-type]
53
+
54
+ # ------------------------------------------------------------------ #
55
+ # Core request handler
56
+ # ------------------------------------------------------------------ #
57
+
58
+ async def handle_async_request(
59
+ self,
60
+ request: httpx.Request,
61
+ ) -> httpx.Response:
62
+ hostname = request.url.host or ""
63
+ scheme = request.url.scheme.lower()
64
+
65
+ # 1-3. Scheme, hostname, and pattern checks (reuse sync validator).
66
+ validate_url_sync(str(request.url), self._policy)
67
+
68
+ # Allowed-hosts bypass - skip DNS/IP validation entirely.
69
+ allowed = {h.lower() for h in _effective_allowed_hosts(self._policy)}
70
+ if hostname.lower() in allowed:
71
+ return await self._inner.handle_async_request(request)
72
+
73
+ # 4. DNS resolution
74
+ port = request.url.port or (443 if scheme == "https" else 80)
75
+ try:
76
+ addrinfo = await asyncio.to_thread(
77
+ socket.getaddrinfo,
78
+ hostname,
79
+ port,
80
+ type=socket.SOCK_STREAM,
81
+ )
82
+ except socket.gaierror as exc:
83
+ msg = "DNS resolution failed"
84
+ raise SSRFBlockedError(msg) from exc
85
+
86
+ if not addrinfo:
87
+ msg = "DNS resolution returned no results"
88
+ raise SSRFBlockedError(msg)
89
+
90
+ # 5. Validate ALL resolved IPs - any blocked means reject.
91
+ for _family, _type, _proto, _canonname, sockaddr in addrinfo:
92
+ ip_str: str = sockaddr[0] # type: ignore[assignment]
93
+ validate_resolved_ip(ip_str, self._policy)
94
+
95
+ # 6. Pin to first resolved IP.
96
+ pinned_ip = addrinfo[0][4][0]
97
+
98
+ # 7. Rewrite URL to use pinned IP, preserving Host header and SNI.
99
+ pinned_url = request.url.copy_with(host=pinned_ip)
100
+
101
+ # Build extensions dict, adding sni_hostname for HTTPS so TLS
102
+ # certificate validation uses the original hostname.
103
+ extensions = dict(request.extensions)
104
+ if scheme == "https":
105
+ extensions["sni_hostname"] = hostname.encode("ascii")
106
+
107
+ pinned_request = httpx.Request(
108
+ method=request.method,
109
+ url=pinned_url,
110
+ headers=request.headers, # Host header already set to original
111
+ content=request.content,
112
+ extensions=extensions,
113
+ )
114
+
115
+ return await self._inner.handle_async_request(pinned_request)
116
+
117
+ # ------------------------------------------------------------------ #
118
+ # Lifecycle
119
+ # ------------------------------------------------------------------ #
120
+
121
+ async def aclose(self) -> None:
122
+ await self._inner.aclose()
123
+
124
+
125
+ # ---------------------------------------------------------------------- #
126
+ # Factory
127
+ # ---------------------------------------------------------------------- #
128
+
129
+
130
+ class SSRFSafeSyncTransport(httpx.BaseTransport):
131
+ """httpx sync transport that validates DNS results against an SSRF policy.
132
+
133
+ Sync mirror of `SSRFSafeTransport`. See that class for full documentation.
134
+ """
135
+
136
+ def __init__(
137
+ self,
138
+ policy: SSRFPolicy = DEFAULT_SSRF_POLICY,
139
+ **transport_kwargs: object,
140
+ ) -> None:
141
+ self._policy = policy
142
+ self._inner = httpx.HTTPTransport(**transport_kwargs) # type: ignore[arg-type]
143
+
144
+ def handle_request(
145
+ self,
146
+ request: httpx.Request,
147
+ ) -> httpx.Response:
148
+ hostname = request.url.host or ""
149
+ scheme = request.url.scheme.lower()
150
+
151
+ validate_url_sync(str(request.url), self._policy)
152
+
153
+ allowed = {h.lower() for h in _effective_allowed_hosts(self._policy)}
154
+ if hostname.lower() in allowed:
155
+ return self._inner.handle_request(request)
156
+
157
+ port = request.url.port or (443 if scheme == "https" else 80)
158
+ try:
159
+ addrinfo = socket.getaddrinfo(
160
+ hostname,
161
+ port,
162
+ type=socket.SOCK_STREAM,
163
+ )
164
+ except socket.gaierror as exc:
165
+ msg = "DNS resolution failed"
166
+ raise SSRFBlockedError(msg) from exc
167
+
168
+ if not addrinfo:
169
+ msg = "DNS resolution returned no results"
170
+ raise SSRFBlockedError(msg)
171
+
172
+ for _family, _type, _proto, _canonname, sockaddr in addrinfo:
173
+ ip_str: str = sockaddr[0] # type: ignore[assignment]
174
+ validate_resolved_ip(ip_str, self._policy)
175
+
176
+ pinned_ip = addrinfo[0][4][0]
177
+ pinned_url = request.url.copy_with(host=pinned_ip)
178
+
179
+ extensions = dict(request.extensions)
180
+ if scheme == "https":
181
+ extensions["sni_hostname"] = hostname.encode("ascii")
182
+
183
+ pinned_request = httpx.Request(
184
+ method=request.method,
185
+ url=pinned_url,
186
+ headers=request.headers,
187
+ content=request.content,
188
+ extensions=extensions,
189
+ )
190
+
191
+ return self._inner.handle_request(pinned_request)
192
+
193
+ def close(self) -> None:
194
+ self._inner.close()
195
+
196
+
197
+ # ---------------------------------------------------------------------- #
198
+ # Factories
199
+ # ---------------------------------------------------------------------- #
200
+
201
+
202
+ def ssrf_safe_client(
203
+ policy: SSRFPolicy = DEFAULT_SSRF_POLICY,
204
+ **kwargs: object,
205
+ ) -> httpx.Client:
206
+ """Create an `httpx.Client` with SSRF protection."""
207
+ transport_kwargs: dict[str, object] = {}
208
+ client_kwargs: dict[str, object] = {}
209
+ for key, value in kwargs.items():
210
+ if key in _TRANSPORT_KWARGS:
211
+ transport_kwargs[key] = value
212
+ else:
213
+ client_kwargs[key] = value
214
+
215
+ transport = SSRFSafeSyncTransport(policy=policy, **transport_kwargs)
216
+
217
+ client_kwargs.setdefault("follow_redirects", True)
218
+ client_kwargs.setdefault("max_redirects", 10)
219
+
220
+ return httpx.Client(
221
+ transport=transport,
222
+ **client_kwargs, # type: ignore[arg-type]
223
+ )
224
+
225
+
226
+ def ssrf_safe_async_client(
227
+ policy: SSRFPolicy = DEFAULT_SSRF_POLICY,
228
+ **kwargs: object,
229
+ ) -> httpx.AsyncClient:
230
+ """Create an `httpx.AsyncClient` with SSRF protection.
231
+
232
+ Drop-in replacement for `httpx.AsyncClient(...)` - callers just swap
233
+ the constructor call. Transport-specific kwargs (`verify`, `cert`,
234
+ `retries`, etc.) are forwarded to the inner `AsyncHTTPTransport`;
235
+ everything else goes to the `AsyncClient`.
236
+ """
237
+ transport_kwargs: dict[str, object] = {}
238
+ client_kwargs: dict[str, object] = {}
239
+ for key, value in kwargs.items():
240
+ if key in _TRANSPORT_KWARGS:
241
+ transport_kwargs[key] = value
242
+ else:
243
+ client_kwargs[key] = value
244
+
245
+ transport = SSRFSafeTransport(policy=policy, **transport_kwargs)
246
+
247
+ # Apply defaults only if not overridden by caller.
248
+ client_kwargs.setdefault("follow_redirects", True)
249
+ client_kwargs.setdefault("max_redirects", 10)
250
+
251
+ return httpx.AsyncClient(
252
+ transport=transport,
253
+ **client_kwargs, # type: ignore[arg-type]
254
+ )
coco_core/agents.py ADDED
@@ -0,0 +1,258 @@
1
+ """Schema definitions for representing agent actions, observations, and return values.
2
+
3
+ !!! warning
4
+
5
+ The schema definitions are provided for backwards compatibility.
6
+
7
+ !!! warning
8
+
9
+ New agents should be built using the
10
+ [`coco` library](https://pypi.org/project/langchain/), which provides a
11
+ simpler and more flexible way to define agents.
12
+
13
+ See docs on [building agents](https://docs.langchain.com/oss/python/langchain/agents).
14
+
15
+ Agents use language models to choose a sequence of actions to take.
16
+
17
+ A basic agent works in the following manner:
18
+
19
+ 1. Given a prompt an agent uses an LLM to request an action to take
20
+ (e.g., a tool to run).
21
+ 2. The agent executes the action (e.g., runs the tool), and receives an observation.
22
+ 3. The agent returns the observation to the LLM, which can then be used to generate
23
+ the next action.
24
+ 4. When the agent reaches a stopping condition, it returns a final return value.
25
+
26
+ The schemas for the agents themselves are defined in `coco.agents.agent`.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ from collections.abc import Sequence
33
+ from typing import Any, Literal
34
+
35
+ from coco_core.load.serializable import Serializable
36
+ from coco_core.messages import (
37
+ AIMessage,
38
+ BaseMessage,
39
+ FunctionMessage,
40
+ HumanMessage,
41
+ )
42
+
43
+
44
+ class AgentAction(Serializable):
45
+ """Represents a request to execute an action by an agent.
46
+
47
+ The action consists of the name of the tool to execute and the input to pass
48
+ to the tool. The log is used to pass along extra information about the action.
49
+ """
50
+
51
+ tool: str
52
+ """The name of the `Tool` to execute."""
53
+
54
+ tool_input: str | dict[Any, Any]
55
+ """The input to pass in to the `Tool`."""
56
+
57
+ log: str
58
+ """Additional information to log about the action.
59
+
60
+ This log can be used in a few ways. First, it can be used to audit what exactly the
61
+ LLM predicted to lead to this `(tool, tool_input)`.
62
+
63
+ Second, it can be used in future iterations to show the LLMs prior thoughts. This is
64
+ useful when `(tool, tool_input)` does not contain full information about the LLM
65
+ prediction (for example, any `thought` before the tool/tool_input).
66
+ """
67
+
68
+ type: Literal["AgentAction"] = "AgentAction"
69
+
70
+ # Override init to support instantiation by position for backward compat.
71
+ def __init__(
72
+ self, tool: str, tool_input: str | dict[Any, Any], log: str, **kwargs: Any
73
+ ):
74
+ """Create an `AgentAction`.
75
+
76
+ Args:
77
+ tool: The name of the tool to execute.
78
+ tool_input: The input to pass in to the `Tool`.
79
+ log: Additional information to log about the action.
80
+ """
81
+ super().__init__(tool=tool, tool_input=tool_input, log=log, **kwargs)
82
+
83
+ @classmethod
84
+ def is_lc_serializable(cls) -> bool:
85
+ """`AgentAction` is serializable.
86
+
87
+ Returns:
88
+ `True`
89
+ """
90
+ return True
91
+
92
+ @classmethod
93
+ def get_lc_namespace(cls) -> list[str]:
94
+ """Get the namespace of the Coco object.
95
+
96
+ Returns:
97
+ `["coco", "schema", "agent"]`
98
+ """
99
+ return ["coco", "schema", "agent"]
100
+
101
+ @property
102
+ def messages(self) -> Sequence[BaseMessage]:
103
+ """Return the messages that correspond to this action."""
104
+ return _convert_agent_action_to_messages(self)
105
+
106
+
107
+ class AgentActionMessageLog(AgentAction):
108
+ """Representation of an action to be executed by an agent.
109
+
110
+ This is similar to `AgentAction`, but includes a message log consisting of
111
+ chat messages.
112
+
113
+ This is useful when working with `ChatModels`, and is used to reconstruct
114
+ conversation history from the agent's perspective.
115
+ """
116
+
117
+ message_log: Sequence[BaseMessage]
118
+ """Similar to log, this can be used to pass along extra information about what exact
119
+ messages were predicted by the LLM before parsing out the `(tool, tool_input)`.
120
+
121
+ This is again useful if `(tool, tool_input)` cannot be used to fully recreate the
122
+ LLM prediction, and you need that LLM prediction (for future agent iteration).
123
+
124
+ Compared to `log`, this is useful when the underlying LLM is a chat model (and
125
+ therefore returns messages rather than a string).
126
+ """
127
+ # Ignoring type because we're overriding the type from AgentAction.
128
+ # And this is the correct thing to do in this case.
129
+ # The type literal is used for serialization purposes.
130
+ type: Literal["AgentActionMessageLog"] = "AgentActionMessageLog" # type: ignore[assignment]
131
+
132
+
133
+ class AgentStep(Serializable):
134
+ """Result of running an `AgentAction`."""
135
+
136
+ action: AgentAction
137
+ """The `AgentAction` that was executed."""
138
+
139
+ observation: Any
140
+ """The result of the `AgentAction`."""
141
+
142
+ @property
143
+ def messages(self) -> Sequence[BaseMessage]:
144
+ """Messages that correspond to this observation."""
145
+ return _convert_agent_observation_to_messages(self.action, self.observation)
146
+
147
+
148
+ class AgentFinish(Serializable):
149
+ """Final return value of an `ActionAgent`.
150
+
151
+ Agents return an `AgentFinish` when they have reached a stopping condition.
152
+ """
153
+
154
+ return_values: dict[Any, Any]
155
+ """Dictionary of return values."""
156
+
157
+ log: str
158
+ """Additional information to log about the return value.
159
+
160
+ This is used to pass along the full LLM prediction, not just the parsed out
161
+ return value.
162
+
163
+ For example, if the full LLM prediction was `Final Answer: 2` you may want to just
164
+ return `2` as a return value, but pass along the full string as a `log` (for
165
+ debugging or observability purposes).
166
+ """
167
+ type: Literal["AgentFinish"] = "AgentFinish"
168
+
169
+ def __init__(self, return_values: dict[Any, Any], log: str, **kwargs: Any):
170
+ """Override init to support instantiation by position for backward compat."""
171
+ super().__init__(return_values=return_values, log=log, **kwargs)
172
+
173
+ @classmethod
174
+ def is_lc_serializable(cls) -> bool:
175
+ """Return `True` as this class is serializable."""
176
+ return True
177
+
178
+ @classmethod
179
+ def get_lc_namespace(cls) -> list[str]:
180
+ """Get the namespace of the Coco object.
181
+
182
+ Returns:
183
+ `["coco", "schema", "agent"]`
184
+ """
185
+ return ["coco", "schema", "agent"]
186
+
187
+ @property
188
+ def messages(self) -> Sequence[BaseMessage]:
189
+ """Messages that correspond to this observation."""
190
+ return [AIMessage(content=self.log)]
191
+
192
+
193
+ def _convert_agent_action_to_messages(
194
+ agent_action: AgentAction,
195
+ ) -> Sequence[BaseMessage]:
196
+ """Convert an agent action to a message.
197
+
198
+ This code is used to reconstruct the original AI message from the agent action.
199
+
200
+ Args:
201
+ agent_action: Agent action to convert.
202
+
203
+ Returns:
204
+ `AIMessage` that corresponds to the original tool invocation.
205
+ """
206
+ if isinstance(agent_action, AgentActionMessageLog):
207
+ return agent_action.message_log
208
+ return [AIMessage(content=agent_action.log)]
209
+
210
+
211
+ def _convert_agent_observation_to_messages(
212
+ agent_action: AgentAction, observation: Any
213
+ ) -> Sequence[BaseMessage]:
214
+ """Convert an agent action to a message.
215
+
216
+ This code is used to reconstruct the original AI message from the agent action.
217
+
218
+ Args:
219
+ agent_action: Agent action to convert.
220
+ observation: Observation to convert to a message.
221
+
222
+ Returns:
223
+ `AIMessage` that corresponds to the original tool invocation.
224
+ """
225
+ if isinstance(agent_action, AgentActionMessageLog):
226
+ return [_create_function_message(agent_action, observation)]
227
+ content = observation
228
+ if not isinstance(observation, str):
229
+ try:
230
+ content = json.dumps(observation, ensure_ascii=False)
231
+ except Exception:
232
+ content = str(observation)
233
+ return [HumanMessage(content=content)]
234
+
235
+
236
+ def _create_function_message(
237
+ agent_action: AgentAction, observation: Any
238
+ ) -> FunctionMessage:
239
+ """Convert agent action and observation into a function message.
240
+
241
+ Args:
242
+ agent_action: the tool invocation request from the agent.
243
+ observation: the result of the tool invocation.
244
+
245
+ Returns:
246
+ `FunctionMessage` that corresponds to the original tool invocation.
247
+ """
248
+ if not isinstance(observation, str):
249
+ try:
250
+ content = json.dumps(observation, ensure_ascii=False)
251
+ except Exception:
252
+ content = str(observation)
253
+ else:
254
+ content = observation
255
+ return FunctionMessage(
256
+ name=agent_action.tool,
257
+ content=content,
258
+ )