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,11 @@
1
+ """Middleware implementations for OpenAI-backed agents."""
2
+
3
+ from coco_openai.middleware.openai_moderation import (
4
+ OpenAIModerationError,
5
+ OpenAIModerationMiddleware,
6
+ )
7
+
8
+ __all__ = [
9
+ "OpenAIModerationError",
10
+ "OpenAIModerationMiddleware",
11
+ ]
@@ -0,0 +1,484 @@
1
+ """Agent middleware that integrates OpenAI's moderation endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Sequence
7
+ from typing import TYPE_CHECKING, Any, Literal, cast
8
+
9
+ from coco.agents.middleware.types import AgentMiddleware, AgentState, hook_config
10
+ from coco_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage
11
+ from openai import AsyncOpenAI, OpenAI
12
+ from openai.types import Moderation, ModerationModel
13
+
14
+ if TYPE_CHECKING: # pragma: no cover
15
+ from coco_graph.runtime import Runtime
16
+
17
+ ViolationStage = Literal["input", "output", "tool"]
18
+
19
+ DEFAULT_VIOLATION_TEMPLATE = (
20
+ "I'm sorry, but I can't comply with that request. It was flagged for {categories}."
21
+ )
22
+
23
+
24
+ class OpenAIModerationError(RuntimeError):
25
+ """Raised when OpenAI flags content and `exit_behavior` is set to `"error"`."""
26
+
27
+ def __init__(
28
+ self,
29
+ *,
30
+ content: str,
31
+ stage: ViolationStage,
32
+ result: Moderation,
33
+ message: str,
34
+ ) -> None:
35
+ """Initialize the error with violation details.
36
+
37
+ Args:
38
+ content: The content that was flagged.
39
+ stage: The stage where the violation occurred.
40
+ result: The moderation result from OpenAI.
41
+ message: The error message.
42
+ """
43
+ super().__init__(message)
44
+ self.content = content
45
+ self.stage = stage
46
+ self.result = result
47
+
48
+
49
+ class OpenAIModerationMiddleware(AgentMiddleware[AgentState[Any], Any]):
50
+ """Moderate agent traffic using OpenAI's moderation endpoint."""
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ model: ModerationModel = "omni-moderation-latest",
56
+ check_input: bool = True,
57
+ check_output: bool = True,
58
+ check_tool_results: bool = False,
59
+ exit_behavior: Literal["error", "end", "replace"] = "end",
60
+ violation_message: str | None = None,
61
+ client: OpenAI | None = None,
62
+ async_client: AsyncOpenAI | None = None,
63
+ ) -> None:
64
+ """Create the middleware instance.
65
+
66
+ Args:
67
+ model: OpenAI moderation model to use.
68
+ check_input: Whether to check user input messages.
69
+ check_output: Whether to check model output messages.
70
+ check_tool_results: Whether to check tool result messages.
71
+ exit_behavior: How to handle violations
72
+ (`'error'`, `'end'`, or `'replace'`).
73
+ violation_message: Custom template for violation messages.
74
+ client: Optional pre-configured OpenAI client to reuse.
75
+ If not provided, a new client will be created.
76
+ async_client: Optional pre-configured AsyncOpenAI client to reuse.
77
+ If not provided, a new async client will be created.
78
+ """
79
+ super().__init__()
80
+ self.model = model
81
+ self.check_input = check_input
82
+ self.check_output = check_output
83
+ self.check_tool_results = check_tool_results
84
+ self.exit_behavior = exit_behavior
85
+ self.violation_message = violation_message
86
+
87
+ self._client = client
88
+ self._async_client = async_client
89
+
90
+ @hook_config(can_jump_to=["end"])
91
+ def before_model(
92
+ self, state: AgentState[Any], runtime: Runtime[Any]
93
+ ) -> dict[str, Any] | None: # type: ignore[override]
94
+ """Moderate user input and tool results before the model is called.
95
+
96
+ Args:
97
+ state: Current agent state containing messages.
98
+ runtime: Agent runtime context.
99
+
100
+ Returns:
101
+ Updated state with moderated messages, or `None` if no changes.
102
+ """
103
+ if not self.check_input and not self.check_tool_results:
104
+ return None
105
+
106
+ messages = list(state.get("messages", []))
107
+ if not messages:
108
+ return None
109
+
110
+ return self._moderate_inputs(messages)
111
+
112
+ @hook_config(can_jump_to=["end"])
113
+ def after_model(
114
+ self, state: AgentState[Any], runtime: Runtime[Any]
115
+ ) -> dict[str, Any] | None: # type: ignore[override]
116
+ """Moderate model output after the model is called.
117
+
118
+ Args:
119
+ state: Current agent state containing messages.
120
+ runtime: Agent runtime context.
121
+
122
+ Returns:
123
+ Updated state with moderated messages, or `None` if no changes.
124
+ """
125
+ if not self.check_output:
126
+ return None
127
+
128
+ messages = list(state.get("messages", []))
129
+ if not messages:
130
+ return None
131
+
132
+ return self._moderate_output(messages)
133
+
134
+ @hook_config(can_jump_to=["end"])
135
+ async def abefore_model(
136
+ self, state: AgentState[Any], runtime: Runtime[Any]
137
+ ) -> dict[str, Any] | None: # type: ignore[override]
138
+ """Async version of before_model.
139
+
140
+ Args:
141
+ state: Current agent state containing messages.
142
+ runtime: Agent runtime context.
143
+
144
+ Returns:
145
+ Updated state with moderated messages, or `None` if no changes.
146
+ """
147
+ if not self.check_input and not self.check_tool_results:
148
+ return None
149
+
150
+ messages = list(state.get("messages", []))
151
+ if not messages:
152
+ return None
153
+
154
+ return await self._amoderate_inputs(messages)
155
+
156
+ @hook_config(can_jump_to=["end"])
157
+ async def aafter_model(
158
+ self, state: AgentState[Any], runtime: Runtime[Any]
159
+ ) -> dict[str, Any] | None: # type: ignore[override]
160
+ """Async version of after_model.
161
+
162
+ Args:
163
+ state: Current agent state containing messages.
164
+ runtime: Agent runtime context.
165
+
166
+ Returns:
167
+ Updated state with moderated messages, or `None` if no changes.
168
+ """
169
+ if not self.check_output:
170
+ return None
171
+
172
+ messages = list(state.get("messages", []))
173
+ if not messages:
174
+ return None
175
+
176
+ return await self._amoderate_output(messages)
177
+
178
+ def _moderate_inputs(
179
+ self, messages: Sequence[BaseMessage]
180
+ ) -> dict[str, Any] | None:
181
+ working = list(messages)
182
+ modified = False
183
+
184
+ if self.check_tool_results:
185
+ action = self._moderate_tool_messages(working)
186
+ if action:
187
+ if "jump_to" in action:
188
+ return action
189
+ working = cast("list[BaseMessage]", action["messages"])
190
+ modified = True
191
+
192
+ if self.check_input:
193
+ action = self._moderate_user_message(working)
194
+ if action:
195
+ if "jump_to" in action:
196
+ return action
197
+ working = cast("list[BaseMessage]", action["messages"])
198
+ modified = True
199
+
200
+ if modified:
201
+ return {"messages": working}
202
+
203
+ return None
204
+
205
+ async def _amoderate_inputs(
206
+ self, messages: Sequence[BaseMessage]
207
+ ) -> dict[str, Any] | None:
208
+ working = list(messages)
209
+ modified = False
210
+
211
+ if self.check_tool_results:
212
+ action = await self._amoderate_tool_messages(working)
213
+ if action:
214
+ if "jump_to" in action:
215
+ return action
216
+ working = cast("list[BaseMessage]", action["messages"])
217
+ modified = True
218
+
219
+ if self.check_input:
220
+ action = await self._amoderate_user_message(working)
221
+ if action:
222
+ if "jump_to" in action:
223
+ return action
224
+ working = cast("list[BaseMessage]", action["messages"])
225
+ modified = True
226
+
227
+ if modified:
228
+ return {"messages": working}
229
+
230
+ return None
231
+
232
+ def _moderate_output(
233
+ self, messages: Sequence[BaseMessage]
234
+ ) -> dict[str, Any] | None:
235
+ last_ai_idx = self._find_last_index(messages, AIMessage)
236
+ if last_ai_idx is None:
237
+ return None
238
+
239
+ ai_message = messages[last_ai_idx]
240
+ text = self._extract_text(ai_message)
241
+ if not text:
242
+ return None
243
+
244
+ result = self._moderate(text)
245
+ if not result.flagged:
246
+ return None
247
+
248
+ return self._apply_violation(
249
+ messages, index=last_ai_idx, stage="output", content=text, result=result
250
+ )
251
+
252
+ async def _amoderate_output(
253
+ self, messages: Sequence[BaseMessage]
254
+ ) -> dict[str, Any] | None:
255
+ last_ai_idx = self._find_last_index(messages, AIMessage)
256
+ if last_ai_idx is None:
257
+ return None
258
+
259
+ ai_message = messages[last_ai_idx]
260
+ text = self._extract_text(ai_message)
261
+ if not text:
262
+ return None
263
+
264
+ result = await self._amoderate(text)
265
+ if not result.flagged:
266
+ return None
267
+
268
+ return self._apply_violation(
269
+ messages, index=last_ai_idx, stage="output", content=text, result=result
270
+ )
271
+
272
+ def _moderate_tool_messages(
273
+ self, messages: Sequence[BaseMessage]
274
+ ) -> dict[str, Any] | None:
275
+ last_ai_idx = self._find_last_index(messages, AIMessage)
276
+ if last_ai_idx is None:
277
+ return None
278
+
279
+ working = list(messages)
280
+ modified = False
281
+
282
+ for idx in range(last_ai_idx + 1, len(working)):
283
+ msg = working[idx]
284
+ if not isinstance(msg, ToolMessage):
285
+ continue
286
+
287
+ text = self._extract_text(msg)
288
+ if not text:
289
+ continue
290
+
291
+ result = self._moderate(text)
292
+ if not result.flagged:
293
+ continue
294
+
295
+ action = self._apply_violation(
296
+ working, index=idx, stage="tool", content=text, result=result
297
+ )
298
+ if action:
299
+ if "jump_to" in action:
300
+ return action
301
+ working = cast("list[BaseMessage]", action["messages"])
302
+ modified = True
303
+
304
+ if modified:
305
+ return {"messages": working}
306
+
307
+ return None
308
+
309
+ async def _amoderate_tool_messages(
310
+ self, messages: Sequence[BaseMessage]
311
+ ) -> dict[str, Any] | None:
312
+ last_ai_idx = self._find_last_index(messages, AIMessage)
313
+ if last_ai_idx is None:
314
+ return None
315
+
316
+ working = list(messages)
317
+ modified = False
318
+
319
+ for idx in range(last_ai_idx + 1, len(working)):
320
+ msg = working[idx]
321
+ if not isinstance(msg, ToolMessage):
322
+ continue
323
+
324
+ text = self._extract_text(msg)
325
+ if not text:
326
+ continue
327
+
328
+ result = await self._amoderate(text)
329
+ if not result.flagged:
330
+ continue
331
+
332
+ action = self._apply_violation(
333
+ working, index=idx, stage="tool", content=text, result=result
334
+ )
335
+ if action:
336
+ if "jump_to" in action:
337
+ return action
338
+ working = cast("list[BaseMessage]", action["messages"])
339
+ modified = True
340
+
341
+ if modified:
342
+ return {"messages": working}
343
+
344
+ return None
345
+
346
+ def _moderate_user_message(
347
+ self, messages: Sequence[BaseMessage]
348
+ ) -> dict[str, Any] | None:
349
+ idx = self._find_last_index(messages, HumanMessage)
350
+ if idx is None:
351
+ return None
352
+
353
+ message = messages[idx]
354
+ text = self._extract_text(message)
355
+ if not text:
356
+ return None
357
+
358
+ result = self._moderate(text)
359
+ if not result.flagged:
360
+ return None
361
+
362
+ return self._apply_violation(
363
+ messages, index=idx, stage="input", content=text, result=result
364
+ )
365
+
366
+ async def _amoderate_user_message(
367
+ self, messages: Sequence[BaseMessage]
368
+ ) -> dict[str, Any] | None:
369
+ idx = self._find_last_index(messages, HumanMessage)
370
+ if idx is None:
371
+ return None
372
+
373
+ message = messages[idx]
374
+ text = self._extract_text(message)
375
+ if not text:
376
+ return None
377
+
378
+ result = await self._amoderate(text)
379
+ if not result.flagged:
380
+ return None
381
+
382
+ return self._apply_violation(
383
+ messages, index=idx, stage="input", content=text, result=result
384
+ )
385
+
386
+ def _apply_violation(
387
+ self,
388
+ messages: Sequence[BaseMessage],
389
+ *,
390
+ index: int | None,
391
+ stage: ViolationStage,
392
+ content: str,
393
+ result: Moderation,
394
+ ) -> dict[str, Any] | None:
395
+ violation_text = self._format_violation_message(content, result)
396
+
397
+ if self.exit_behavior == "error":
398
+ raise OpenAIModerationError(
399
+ content=content,
400
+ stage=stage,
401
+ result=result,
402
+ message=violation_text,
403
+ )
404
+
405
+ if self.exit_behavior == "end":
406
+ return {"jump_to": "end", "messages": [AIMessage(content=violation_text)]}
407
+
408
+ if index is None:
409
+ return None
410
+
411
+ new_messages = list(messages)
412
+ original = new_messages[index]
413
+ new_messages[index] = cast(
414
+ BaseMessage, original.model_copy(update={"content": violation_text})
415
+ )
416
+ return {"messages": new_messages}
417
+
418
+ def _moderate(self, text: str) -> Moderation:
419
+ if self._client is None:
420
+ self._client = self._build_client()
421
+ response = self._client.moderations.create(model=self.model, input=text)
422
+ return response.results[0]
423
+
424
+ async def _amoderate(self, text: str) -> Moderation:
425
+ if self._async_client is None:
426
+ self._async_client = self._build_async_client()
427
+ response = await self._async_client.moderations.create(
428
+ model=self.model, input=text
429
+ )
430
+ return response.results[0]
431
+
432
+ def _build_client(self) -> OpenAI:
433
+ self._client = OpenAI()
434
+ return self._client
435
+
436
+ def _build_async_client(self) -> AsyncOpenAI:
437
+ self._async_client = AsyncOpenAI()
438
+ return self._async_client
439
+
440
+ def _format_violation_message(self, content: str, result: Moderation) -> str:
441
+ # Convert categories to dict and filter for flagged items
442
+ categories_dict = result.categories.model_dump()
443
+ categories = [
444
+ name.replace("_", " ")
445
+ for name, flagged in categories_dict.items()
446
+ if flagged
447
+ ]
448
+ category_label = (
449
+ ", ".join(categories) if categories else "OpenAI's safety policies"
450
+ )
451
+ template = self.violation_message or DEFAULT_VIOLATION_TEMPLATE
452
+ scores_json = json.dumps(result.category_scores.model_dump(), sort_keys=True)
453
+ try:
454
+ message = template.format(
455
+ categories=category_label,
456
+ category_scores=scores_json,
457
+ original_content=content,
458
+ )
459
+ except KeyError:
460
+ message = template
461
+ return message
462
+
463
+ def _find_last_index(
464
+ self, messages: Sequence[BaseMessage], message_type: type[BaseMessage]
465
+ ) -> int | None:
466
+ for idx in range(len(messages) - 1, -1, -1):
467
+ if isinstance(messages[idx], message_type):
468
+ return idx
469
+ return None
470
+
471
+ def _extract_text(self, message: BaseMessage) -> str | None:
472
+ if message.content is None:
473
+ return None
474
+ text_accessor = getattr(message, "text", None)
475
+ if text_accessor is None:
476
+ return str(message.content)
477
+ text = str(text_accessor)
478
+ return text if text else None
479
+
480
+
481
+ __all__ = [
482
+ "OpenAIModerationError",
483
+ "OpenAIModerationMiddleware",
484
+ ]
@@ -0,0 +1,9 @@
1
+ """Output parsers for OpenAI tools."""
2
+
3
+ from coco_core.output_parsers.openai_tools import (
4
+ JsonOutputKeyToolsParser,
5
+ JsonOutputToolsParser,
6
+ PydanticToolsParser,
7
+ )
8
+
9
+ __all__ = ["JsonOutputKeyToolsParser", "JsonOutputToolsParser", "PydanticToolsParser"]
@@ -0,0 +1,9 @@
1
+ """Output parsers for OpenAI tools."""
2
+
3
+ from coco_core.output_parsers.openai_tools import (
4
+ JsonOutputKeyToolsParser,
5
+ JsonOutputToolsParser,
6
+ PydanticToolsParser,
7
+ )
8
+
9
+ __all__ = ["JsonOutputKeyToolsParser", "JsonOutputToolsParser", "PydanticToolsParser"]
coco_openai/py.typed ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ """Tools package for OpenAI integrations."""
2
+
3
+ from coco_openai.tools.custom_tool import custom_tool
4
+
5
+ __all__ = ["custom_tool"]
@@ -0,0 +1,111 @@
1
+ """Custom tool decorator for OpenAI custom tools."""
2
+
3
+ import inspect
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import Any
6
+
7
+ from coco_core.tools import tool
8
+
9
+
10
+ def _make_wrapped_func(func: Callable[..., str]) -> Callable[..., list[dict[str, Any]]]:
11
+ def wrapped(x: str) -> list[dict[str, Any]]:
12
+ return [{"type": "custom_tool_call_output", "output": func(x)}]
13
+
14
+ return wrapped
15
+
16
+
17
+ def _make_wrapped_coroutine(
18
+ coroutine: Callable[..., Awaitable[str]],
19
+ ) -> Callable[..., Awaitable[list[dict[str, Any]]]]:
20
+ async def wrapped(*args: Any, **kwargs: Any) -> list[dict[str, Any]]:
21
+ result = await coroutine(*args, **kwargs)
22
+ return [{"type": "custom_tool_call_output", "output": result}]
23
+
24
+ return wrapped
25
+
26
+
27
+ def custom_tool(*args: Any, **kwargs: Any) -> Any:
28
+ """Decorator to create an OpenAI custom tool.
29
+
30
+ Custom tools allow for tools with (potentially long) freeform string inputs.
31
+
32
+ See below for an example using CocoGraph:
33
+
34
+ ```python
35
+ @custom_tool
36
+ def execute_code(code: str) -> str:
37
+ \"\"\"Execute python code.\"\"\"
38
+ return "27"
39
+
40
+
41
+ model = ChatOpenAI(model="gpt-5", output_version="responses/v1")
42
+
43
+ agent = create_react_agent(model, [execute_code])
44
+
45
+ input_message = {"role": "user", "content": "Use the tool to calculate 3^3."}
46
+ for step in agent.stream(
47
+ {"messages": [input_message]},
48
+ stream_mode="values",
49
+ ):
50
+ step["messages"][-1].pretty_print()
51
+ ```
52
+
53
+ You can also specify a format for a corresponding context-free grammar using the
54
+ `format` kwarg:
55
+
56
+ ```python
57
+ from coco_openai import ChatOpenAI, custom_tool
58
+ from coco_graph.prebuilt import create_react_agent
59
+
60
+ grammar = \"\"\"
61
+ start: expr
62
+ expr: term (SP ADD SP term)* -> add
63
+ | term
64
+ term: factor (SP MUL SP factor)* -> mul
65
+ | factor
66
+ factor: INT
67
+ SP: " "
68
+ ADD: "+"
69
+ MUL: "*"
70
+ %import common.INT
71
+ \"\"\"
72
+
73
+ format = {"type": "grammar", "syntax": "lark", "definition": grammar}
74
+
75
+ # highlight-next-line
76
+ @custom_tool(format=format)
77
+ def do_math(input_string: str) -> str:
78
+ \"\"\"Do a mathematical operation.\"\"\"
79
+ return "27"
80
+
81
+
82
+ model = ChatOpenAI(model="gpt-5", output_version="responses/v1")
83
+
84
+ agent = create_react_agent(model, [do_math])
85
+
86
+ input_message = {"role": "user", "content": "Use the tool to calculate 3^3."}
87
+ for step in agent.stream(
88
+ {"messages": [input_message]},
89
+ stream_mode="values",
90
+ ):
91
+ step["messages"][-1].pretty_print()
92
+ ```
93
+ """
94
+
95
+ def decorator(func: Callable[..., Any]) -> Any:
96
+ metadata = {"type": "custom_tool"}
97
+ if "format" in kwargs:
98
+ metadata["format"] = kwargs.pop("format")
99
+ tool_obj = tool(infer_schema=False, **kwargs)(func)
100
+ tool_obj.metadata = metadata
101
+ tool_obj.description = func.__doc__
102
+ if inspect.iscoroutinefunction(func):
103
+ tool_obj.coroutine = _make_wrapped_coroutine(func)
104
+ else:
105
+ tool_obj.func = _make_wrapped_func(func)
106
+ return tool_obj
107
+
108
+ if args and callable(args[0]) and not kwargs:
109
+ return decorator(args[0])
110
+
111
+ return decorator