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,454 @@
1
+ """Shared redaction utilities for middleware components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import ipaddress
7
+ import operator
8
+ import re
9
+ from collections.abc import Callable, Sequence
10
+ from dataclasses import dataclass
11
+ from typing import Literal
12
+ from urllib.parse import urlparse
13
+
14
+ from typing_extensions import TypedDict
15
+
16
+ RedactionStrategy = Literal["block", "redact", "mask", "hash"]
17
+ """Supported strategies for handling detected sensitive values."""
18
+
19
+
20
+ class PIIMatch(TypedDict):
21
+ """Represents an individual match of sensitive data."""
22
+
23
+ type: str
24
+ value: str
25
+ start: int
26
+ end: int
27
+
28
+
29
+ class PIIDetectionError(Exception):
30
+ """Raised when configured to block on detected sensitive values."""
31
+
32
+ def __init__(self, pii_type: str, matches: Sequence[PIIMatch]) -> None:
33
+ """Initialize the exception with match context.
34
+
35
+ Args:
36
+ pii_type: Name of the detected sensitive type.
37
+ matches: All matches that were detected for that type.
38
+ """
39
+ self.pii_type = pii_type
40
+ self.matches = list(matches)
41
+ count = len(matches)
42
+ msg = f"Detected {count} instance(s) of {pii_type} in text content"
43
+ super().__init__(msg)
44
+
45
+
46
+ Detector = Callable[[str], list[PIIMatch]]
47
+ """Callable signature for detectors that locate sensitive values."""
48
+
49
+
50
+ def detect_email(content: str) -> list[PIIMatch]:
51
+ """Detect email addresses in content.
52
+
53
+ Args:
54
+ content: The text content to scan for email addresses.
55
+
56
+ Returns:
57
+ A list of detected email matches.
58
+ """
59
+ pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
60
+ return [
61
+ PIIMatch(
62
+ type="email",
63
+ value=match.group(),
64
+ start=match.start(),
65
+ end=match.end(),
66
+ )
67
+ for match in re.finditer(pattern, content)
68
+ ]
69
+
70
+
71
+ def detect_credit_card(content: str) -> list[PIIMatch]:
72
+ """Detect credit card numbers in content using Luhn validation.
73
+
74
+ Args:
75
+ content: The text content to scan for credit card numbers.
76
+
77
+ Returns:
78
+ A list of detected credit card matches.
79
+ """
80
+ pattern = r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b"
81
+ matches = []
82
+
83
+ for match in re.finditer(pattern, content):
84
+ card_number = match.group()
85
+ if _passes_luhn(card_number):
86
+ matches.append(
87
+ PIIMatch(
88
+ type="credit_card",
89
+ value=card_number,
90
+ start=match.start(),
91
+ end=match.end(),
92
+ )
93
+ )
94
+
95
+ return matches
96
+
97
+
98
+ def detect_ip(content: str) -> list[PIIMatch]:
99
+ """Detect IPv4 or IPv6 addresses in content.
100
+
101
+ Args:
102
+ content: The text content to scan for IP addresses.
103
+
104
+ Returns:
105
+ A list of detected IP address matches.
106
+ """
107
+ matches: list[PIIMatch] = []
108
+ ipv4_pattern = r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b"
109
+
110
+ for match in re.finditer(ipv4_pattern, content):
111
+ ip_candidate = match.group()
112
+ try:
113
+ ipaddress.ip_address(ip_candidate)
114
+ except ValueError:
115
+ continue
116
+ matches.append(
117
+ PIIMatch(
118
+ type="ip",
119
+ value=ip_candidate,
120
+ start=match.start(),
121
+ end=match.end(),
122
+ )
123
+ )
124
+
125
+ return matches
126
+
127
+
128
+ def detect_mac_address(content: str) -> list[PIIMatch]:
129
+ """Detect MAC addresses in content.
130
+
131
+ Args:
132
+ content: The text content to scan for MAC addresses.
133
+
134
+ Returns:
135
+ A list of detected MAC address matches.
136
+ """
137
+ pattern = r"\b([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b"
138
+ return [
139
+ PIIMatch(
140
+ type="mac_address",
141
+ value=match.group(),
142
+ start=match.start(),
143
+ end=match.end(),
144
+ )
145
+ for match in re.finditer(pattern, content)
146
+ ]
147
+
148
+
149
+ def detect_url(content: str) -> list[PIIMatch]:
150
+ """Detect URLs in content using regex and stdlib validation.
151
+
152
+ Args:
153
+ content: The text content to scan for URLs.
154
+
155
+ Returns:
156
+ A list of detected URL matches.
157
+ """
158
+ matches: list[PIIMatch] = []
159
+
160
+ # Pattern 1: URLs with scheme (http:// or https://)
161
+ scheme_pattern = r"https?://[^\s<>\"{}|\\^`\[\]]+"
162
+
163
+ for match in re.finditer(scheme_pattern, content):
164
+ url = match.group()
165
+ result = urlparse(url)
166
+ if result.scheme in {"http", "https"} and result.netloc:
167
+ matches.append(
168
+ PIIMatch(
169
+ type="url",
170
+ value=url,
171
+ start=match.start(),
172
+ end=match.end(),
173
+ )
174
+ )
175
+
176
+ # Pattern 2: URLs without scheme (www.example.com or example.com/path)
177
+ # More conservative to avoid false positives
178
+ bare_pattern = (
179
+ r"\b(?:www\.)?[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"
180
+ r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?:/[^\s]*)?"
181
+ )
182
+
183
+ for match in re.finditer(bare_pattern, content):
184
+ start, end = match.start(), match.end()
185
+ # Skip if already matched with scheme
186
+ if any(m["start"] <= start < m["end"] or m["start"] < end <= m["end"] for m in matches):
187
+ continue
188
+
189
+ url = match.group()
190
+ # Only accept if it has a path or starts with www
191
+ # This reduces false positives like "example.com" in prose
192
+ if "/" in url or url.startswith("www."):
193
+ # Add scheme for validation (required for urlparse to work correctly)
194
+ test_url = f"http://{url}"
195
+ result = urlparse(test_url)
196
+ if result.netloc and "." in result.netloc:
197
+ matches.append(
198
+ PIIMatch(
199
+ type="url",
200
+ value=url,
201
+ start=start,
202
+ end=end,
203
+ )
204
+ )
205
+
206
+ return matches
207
+
208
+
209
+ BUILTIN_DETECTORS: dict[str, Detector] = {
210
+ "email": detect_email,
211
+ "credit_card": detect_credit_card,
212
+ "ip": detect_ip,
213
+ "mac_address": detect_mac_address,
214
+ "url": detect_url,
215
+ }
216
+ """Registry of built-in detectors keyed by type name."""
217
+
218
+ _CARD_NUMBER_MIN_DIGITS = 13
219
+ _CARD_NUMBER_MAX_DIGITS = 19
220
+
221
+
222
+ def _passes_luhn(card_number: str) -> bool:
223
+ """Validate credit card number using the Luhn checksum."""
224
+ digits = [int(d) for d in card_number if d.isdigit()]
225
+ if not _CARD_NUMBER_MIN_DIGITS <= len(digits) <= _CARD_NUMBER_MAX_DIGITS:
226
+ return False
227
+
228
+ checksum = 0
229
+ for index, digit in enumerate(reversed(digits)):
230
+ value = digit
231
+ if index % 2 == 1:
232
+ value *= 2
233
+ if value > 9: # noqa: PLR2004
234
+ value -= 9
235
+ checksum += value
236
+ return checksum % 10 == 0
237
+
238
+
239
+ def _apply_redact_strategy(content: str, matches: list[PIIMatch]) -> str:
240
+ result = content
241
+ for match in sorted(matches, key=operator.itemgetter("start"), reverse=True):
242
+ replacement = f"[REDACTED_{match['type'].upper()}]"
243
+ result = result[: match["start"]] + replacement + result[match["end"] :]
244
+ return result
245
+
246
+
247
+ _UNMASKED_CHAR_NUMBER = 4
248
+ _IPV4_PARTS_NUMBER = 4
249
+
250
+
251
+ def _apply_mask_strategy(content: str, matches: list[PIIMatch]) -> str:
252
+ result = content
253
+ for match in sorted(matches, key=operator.itemgetter("start"), reverse=True):
254
+ value = match["value"]
255
+ pii_type = match["type"]
256
+ if pii_type == "email":
257
+ parts = value.split("@")
258
+ if len(parts) == 2: # noqa: PLR2004
259
+ domain_parts = parts[1].split(".")
260
+ masked = (
261
+ f"{parts[0]}@****.{domain_parts[-1]}"
262
+ if len(domain_parts) > 1
263
+ else f"{parts[0]}@****"
264
+ )
265
+ else:
266
+ masked = "****"
267
+ elif pii_type == "credit_card":
268
+ digits_only = "".join(c for c in value if c.isdigit())
269
+ separator = "-" if "-" in value else " " if " " in value else ""
270
+ if separator:
271
+ masked = (
272
+ f"****{separator}****{separator}****{separator}"
273
+ f"{digits_only[-_UNMASKED_CHAR_NUMBER:]}"
274
+ )
275
+ else:
276
+ masked = f"************{digits_only[-_UNMASKED_CHAR_NUMBER:]}"
277
+ elif pii_type == "ip":
278
+ octets = value.split(".")
279
+ masked = f"*.*.*.{octets[-1]}" if len(octets) == _IPV4_PARTS_NUMBER else "****"
280
+ elif pii_type == "mac_address":
281
+ separator = ":" if ":" in value else "-"
282
+ masked = (
283
+ f"**{separator}**{separator}**{separator}**{separator}**{separator}{value[-2:]}"
284
+ )
285
+ elif pii_type == "url":
286
+ masked = "[MASKED_URL]"
287
+ else:
288
+ masked = (
289
+ f"****{value[-_UNMASKED_CHAR_NUMBER:]}"
290
+ if len(value) > _UNMASKED_CHAR_NUMBER
291
+ else "****"
292
+ )
293
+ result = result[: match["start"]] + masked + result[match["end"] :]
294
+ return result
295
+
296
+
297
+ def _apply_hash_strategy(content: str, matches: list[PIIMatch]) -> str:
298
+ result = content
299
+ for match in sorted(matches, key=operator.itemgetter("start"), reverse=True):
300
+ digest = hashlib.sha256(match["value"].encode()).hexdigest()[:8]
301
+ replacement = f"<{match['type']}_hash:{digest}>"
302
+ result = result[: match["start"]] + replacement + result[match["end"] :]
303
+ return result
304
+
305
+
306
+ def apply_strategy(
307
+ content: str,
308
+ matches: list[PIIMatch],
309
+ strategy: RedactionStrategy,
310
+ ) -> str:
311
+ """Apply the configured strategy to matches within content.
312
+
313
+ Args:
314
+ content: The content to apply strategy to.
315
+ matches: List of detected PII matches.
316
+ strategy: The redaction strategy to apply.
317
+
318
+ Returns:
319
+ The content with the strategy applied.
320
+
321
+ Raises:
322
+ PIIDetectionError: If the strategy is `'block'` and matches are found.
323
+ ValueError: If the strategy is unknown.
324
+ """
325
+ if not matches:
326
+ return content
327
+ if strategy == "redact":
328
+ return _apply_redact_strategy(content, matches)
329
+ if strategy == "mask":
330
+ return _apply_mask_strategy(content, matches)
331
+ if strategy == "hash":
332
+ return _apply_hash_strategy(content, matches)
333
+ if strategy == "block":
334
+ raise PIIDetectionError(matches[0]["type"], matches)
335
+ msg = f"Unknown redaction strategy: {strategy}" # type: ignore[unreachable]
336
+ raise ValueError(msg)
337
+
338
+
339
+ def resolve_detector(pii_type: str, detector: Detector | str | None) -> Detector:
340
+ """Return a callable detector for the given configuration.
341
+
342
+ Args:
343
+ pii_type: The PII type name.
344
+ detector: Optional custom detector or regex pattern. If `None`, a built-in detector
345
+ for the given PII type will be used.
346
+
347
+ Returns:
348
+ The resolved detector.
349
+
350
+ Raises:
351
+ ValueError: If an unknown PII type is specified without a custom detector or regex.
352
+ """
353
+ if detector is None:
354
+ if pii_type not in BUILTIN_DETECTORS:
355
+ msg = (
356
+ f"Unknown PII type: {pii_type}. "
357
+ f"Must be one of {list(BUILTIN_DETECTORS.keys())} or provide a custom detector."
358
+ )
359
+ raise ValueError(msg)
360
+ return BUILTIN_DETECTORS[pii_type]
361
+ if isinstance(detector, str):
362
+ pattern = re.compile(detector)
363
+
364
+ def regex_detector(content: str) -> list[PIIMatch]:
365
+ return [
366
+ PIIMatch(
367
+ type=pii_type,
368
+ value=match.group(),
369
+ start=match.start(),
370
+ end=match.end(),
371
+ )
372
+ for match in pattern.finditer(content)
373
+ ]
374
+
375
+ return regex_detector
376
+
377
+ # Wrap the custom callable to normalize its output.
378
+ # Custom detectors may return dicts with "text" instead of "value"
379
+ # and may omit "type". Map them to proper PIIMatch objects so that
380
+ # downstream strategies (hash, mask) can access match["value"].
381
+ raw_detector = detector
382
+
383
+ def _normalizing_detector(content: str) -> list[PIIMatch]:
384
+ return [
385
+ PIIMatch(
386
+ type=m.get("type", pii_type),
387
+ value=m.get("value", m.get("text", "")),
388
+ start=m["start"],
389
+ end=m["end"],
390
+ )
391
+ for m in raw_detector(content)
392
+ ]
393
+
394
+ return _normalizing_detector
395
+
396
+
397
+ @dataclass(frozen=True)
398
+ class RedactionRule:
399
+ """Configuration for handling a single PII type."""
400
+
401
+ pii_type: str
402
+ strategy: RedactionStrategy = "redact"
403
+ detector: Detector | str | None = None
404
+
405
+ def resolve(self) -> ResolvedRedactionRule:
406
+ """Resolve runtime detector and return an immutable rule.
407
+
408
+ Returns:
409
+ The resolved redaction rule.
410
+ """
411
+ resolved_detector = resolve_detector(self.pii_type, self.detector)
412
+ return ResolvedRedactionRule(
413
+ pii_type=self.pii_type,
414
+ strategy=self.strategy,
415
+ detector=resolved_detector,
416
+ )
417
+
418
+
419
+ @dataclass(frozen=True)
420
+ class ResolvedRedactionRule:
421
+ """Resolved redaction rule ready for execution."""
422
+
423
+ pii_type: str
424
+ strategy: RedactionStrategy
425
+ detector: Detector
426
+
427
+ def apply(self, content: str) -> tuple[str, list[PIIMatch]]:
428
+ """Apply this rule to content, returning new content and matches.
429
+
430
+ Args:
431
+ content: The text content to scan and redact.
432
+
433
+ Returns:
434
+ A tuple of (updated content, list of detected matches).
435
+ """
436
+ matches = self.detector(content)
437
+ if not matches:
438
+ return content, []
439
+ updated = apply_strategy(content, matches, self.strategy)
440
+ return updated, matches
441
+
442
+
443
+ __all__ = [
444
+ "PIIDetectionError",
445
+ "PIIMatch",
446
+ "RedactionRule",
447
+ "ResolvedRedactionRule",
448
+ "apply_strategy",
449
+ "detect_credit_card",
450
+ "detect_email",
451
+ "detect_ip",
452
+ "detect_mac_address",
453
+ "detect_url",
454
+ ]
@@ -0,0 +1,134 @@
1
+ """Shared retry utilities for agent middleware.
2
+
3
+ This module contains common constants, utilities, and logic used by both
4
+ model and tool retry middleware implementations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import random
10
+ from collections.abc import Callable
11
+ from typing import Literal
12
+
13
+ from coco_core.exceptions import ModelError
14
+
15
+ # Type aliases
16
+ RetryOn = tuple[type[Exception], ...] | Callable[[Exception], bool]
17
+ """Type for specifying which exceptions to retry on.
18
+
19
+ Can be either:
20
+ - A tuple of exception types to retry on (based on `isinstance` checks)
21
+ - A callable that takes an exception and returns `True` if it should be retried
22
+ """
23
+
24
+ OnFailure = Literal["error", "continue"] | Callable[[Exception], str]
25
+ """Type for specifying failure handling behavior.
26
+
27
+ Can be either:
28
+ - A literal action string (`'error'` or `'continue'`)
29
+ - `'error'`: Re-raise the exception, stopping agent execution.
30
+ - `'continue'`: Inject a message with the error details, allowing the agent to continue.
31
+
32
+ For tool retries, a `ToolMessage` with the error details will be injected.
33
+
34
+ For model retries, an `AIMessage` with the error details will be returned.
35
+ - A callable that takes an exception and returns a string for error message content
36
+ """
37
+
38
+
39
+ def validate_retry_params(
40
+ max_retries: int,
41
+ initial_delay: float,
42
+ max_delay: float,
43
+ backoff_factor: float,
44
+ ) -> None:
45
+ """Validate retry parameters.
46
+
47
+ Args:
48
+ max_retries: Maximum number of retry attempts.
49
+ initial_delay: Initial delay in seconds before first retry.
50
+ max_delay: Maximum delay in seconds between retries.
51
+ backoff_factor: Multiplier for exponential backoff.
52
+
53
+ Raises:
54
+ ValueError: If any parameter is invalid (negative values).
55
+ """
56
+ if max_retries < 0:
57
+ msg = "max_retries must be >= 0"
58
+ raise ValueError(msg)
59
+ if initial_delay < 0:
60
+ msg = "initial_delay must be >= 0"
61
+ raise ValueError(msg)
62
+ if max_delay < 0:
63
+ msg = "max_delay must be >= 0"
64
+ raise ValueError(msg)
65
+ if backoff_factor < 0:
66
+ msg = "backoff_factor must be >= 0"
67
+ raise ValueError(msg)
68
+
69
+
70
+ def default_retry_on(exc: Exception) -> bool:
71
+ """Return whether an exception should be retried by default."""
72
+ if isinstance(exc, ModelError):
73
+ return exc.is_retryable
74
+ return True
75
+
76
+
77
+ def should_retry_exception(
78
+ exc: Exception,
79
+ retry_on: RetryOn,
80
+ ) -> bool:
81
+ """Check if an exception should trigger a retry.
82
+
83
+ Args:
84
+ exc: The exception that occurred.
85
+ retry_on: Either a tuple of exception types to retry on, or a callable
86
+ that takes an exception and returns `True` if it should be retried.
87
+
88
+ Returns:
89
+ `True` if the exception should be retried, `False` otherwise.
90
+ """
91
+ if callable(retry_on):
92
+ return retry_on(exc)
93
+ return isinstance(exc, retry_on)
94
+
95
+
96
+ def calculate_delay(
97
+ retry_number: int,
98
+ *,
99
+ backoff_factor: float,
100
+ initial_delay: float,
101
+ max_delay: float,
102
+ jitter: bool,
103
+ ) -> float:
104
+ """Calculate delay for a retry attempt with exponential backoff and optional jitter.
105
+
106
+ Args:
107
+ retry_number: The retry attempt number (0-indexed).
108
+ backoff_factor: Multiplier for exponential backoff.
109
+
110
+ Set to `0.0` for constant delay.
111
+ initial_delay: Initial delay in seconds before first retry.
112
+ max_delay: Maximum delay in seconds between retries.
113
+
114
+ Caps exponential backoff growth.
115
+ jitter: Whether to add random jitter to delay to avoid thundering herd.
116
+
117
+ Returns:
118
+ Delay in seconds before next retry.
119
+ """
120
+ if backoff_factor == 0.0:
121
+ delay = initial_delay
122
+ else:
123
+ delay = initial_delay * (backoff_factor**retry_number)
124
+
125
+ # Cap at max_delay
126
+ delay = min(delay, max_delay)
127
+
128
+ if jitter and delay > 0:
129
+ jitter_amount = delay * 0.25 # ±25% jitter
130
+ delay += random.uniform(-jitter_amount, jitter_amount) # noqa: S311
131
+ # Ensure delay is not negative after jitter
132
+ delay = max(0, delay)
133
+
134
+ return delay
@@ -0,0 +1,61 @@
1
+ """Process-global default `TracePolicy` for agent middleware.
2
+
3
+ `TracePolicy` (from coco_graph) shapes what a middleware hook span records. This module
4
+ adds a process-wide default that individual middleware override via their own
5
+ `trace_policy` attribute. Set it once at startup with `configure_trace_policy`; the
6
+ default is read at span-creation time, so it applies to agents built before or after
7
+ the call.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Any, Literal
13
+
14
+ from coco_graph.types import TracePolicy
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Callable
18
+
19
+ _DEFAULT_TRACE_POLICY: TracePolicy | None = None
20
+
21
+
22
+ def configure_trace_policy(policy: TracePolicy | None) -> None:
23
+ """Set the process-wide default `TracePolicy` for agent middleware hook spans.
24
+
25
+ Call once at startup. A middleware's own `trace_policy` overrides this wholesale
26
+ (no field-level merge). Pass `None` to clear the default.
27
+ """
28
+ global _DEFAULT_TRACE_POLICY # noqa: PLW0603 - process-wide default, set once at startup
29
+ _DEFAULT_TRACE_POLICY = policy
30
+
31
+
32
+ def _resolve(mw_policy: TracePolicy | None) -> TracePolicy | None:
33
+ """The effective policy: the middleware's own if set, else the global default."""
34
+ return mw_policy if mw_policy is not None else _DEFAULT_TRACE_POLICY
35
+
36
+
37
+ def _resolved_transform(
38
+ mw_policy: TracePolicy | None,
39
+ field: Literal["process_inputs", "process_outputs"],
40
+ ) -> Callable[[Any], Any]:
41
+ """A trace processor that resolves the effective policy at call time.
42
+
43
+ Reading the global at call time (not build time) means `configure_trace_policy` can
44
+ run after `create_agent` and still apply. Passes the value through unchanged when no
45
+ effective policy defines `field`.
46
+ """
47
+
48
+ def process(value: Any) -> Any:
49
+ effective = _resolve(mw_policy)
50
+ fn = getattr(effective, field) if effective is not None else None
51
+ return fn(value) if fn is not None else value
52
+
53
+ return process
54
+
55
+
56
+ def _node_trace_policy(mw_policy: TracePolicy | None) -> TracePolicy:
57
+ """Build the resolver-backed `TracePolicy` for a middleware's node hook."""
58
+ return TracePolicy(
59
+ process_inputs=_resolved_transform(mw_policy, "process_inputs"),
60
+ process_outputs=_resolved_transform(mw_policy, "process_outputs"),
61
+ )