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,379 @@
1
+ """`Runnable` that retries a `Runnable` if it fails."""
2
+
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ TypeVar,
7
+ cast,
8
+ )
9
+
10
+ from tenacity import (
11
+ AsyncRetrying,
12
+ RetryCallState,
13
+ RetryError,
14
+ Retrying,
15
+ retry_if_exception_type,
16
+ stop_after_attempt,
17
+ wait_exponential_jitter,
18
+ )
19
+ from typing_extensions import TypedDict, override
20
+
21
+ from coco_core.runnables.base import RunnableBindingBase
22
+ from coco_core.runnables.config import RunnableConfig, patch_config
23
+ from coco_core.runnables.utils import Input, Output
24
+
25
+ if TYPE_CHECKING:
26
+ from coco_core.callbacks.manager import (
27
+ AsyncCallbackManagerForChainRun,
28
+ CallbackManagerForChainRun,
29
+ )
30
+
31
+ T = TypeVar("T", CallbackManagerForChainRun, AsyncCallbackManagerForChainRun)
32
+ U = TypeVar("U")
33
+
34
+
35
+ class ExponentialJitterParams(TypedDict, total=False):
36
+ """Parameters for `tenacity.wait_exponential_jitter`."""
37
+
38
+ initial: float
39
+ """Initial wait."""
40
+ max: float
41
+ """Maximum wait."""
42
+ exp_base: float
43
+ """Base for exponential backoff."""
44
+ jitter: float
45
+ """Random additional wait sampled from random.uniform(0, jitter)."""
46
+
47
+
48
+ class RunnableRetry(RunnableBindingBase[Input, Output]): # type: ignore[no-redef]
49
+ """Retry a Runnable if it fails.
50
+
51
+ RunnableRetry can be used to add retry logic to any object
52
+ that subclasses the base Runnable.
53
+
54
+ Such retries are especially useful for network calls that may fail
55
+ due to transient errors.
56
+
57
+ The RunnableRetry is implemented as a RunnableBinding. The easiest
58
+ way to use it is through the `.with_retry()` method on all Runnables.
59
+
60
+ Example:
61
+ Here's an example that uses a RunnableLambda to raise an exception
62
+
63
+ ```python
64
+ import time
65
+
66
+
67
+ def foo(input) -> None:
68
+ '''Fake function that raises an exception.'''
69
+ raise ValueError(f"Invoking foo failed. At time {time.time()}")
70
+
71
+
72
+ runnable = RunnableLambda(foo)
73
+
74
+ runnable_with_retries = runnable.with_retry(
75
+ retry_if_exception_type=(ValueError,), # Retry only on ValueError
76
+ wait_exponential_jitter=True, # Add jitter to the exponential backoff
77
+ stop_after_attempt=2, # Try twice
78
+ exponential_jitter_params={"initial": 2}, # if desired, customize backoff
79
+ )
80
+
81
+ # The method invocation above is equivalent to the longer form below:
82
+
83
+ runnable_with_retries = RunnableRetry(
84
+ bound=runnable,
85
+ retry_exception_types=(ValueError,),
86
+ max_attempt_number=2,
87
+ wait_exponential_jitter=True,
88
+ exponential_jitter_params={"initial": 2},
89
+ )
90
+ ```
91
+
92
+ This logic can be used to retry any Runnable, including a chain of Runnables,
93
+ but in general it's best practice to keep the scope of the retry as small as
94
+ possible. For example, if you have a chain of Runnables, you should only retry
95
+ the Runnable that is likely to fail, not the entire chain.
96
+
97
+ Example:
98
+ ```python
99
+ from coco_openai import ChatOpenAI
100
+ from coco_core.prompts import PromptTemplate
101
+
102
+ template = PromptTemplate.from_template("tell me a joke about {topic}.")
103
+ model = ChatOpenAI(temperature=0.5)
104
+
105
+ # Good
106
+ chain = template | model.with_retry()
107
+
108
+ # Bad
109
+ chain = template | model
110
+ retryable_chain = chain.with_retry()
111
+ ```
112
+ """
113
+
114
+ retry_exception_types: tuple[type[BaseException], ...] = (Exception,)
115
+ """The exception types to retry on. By default all exceptions are retried.
116
+
117
+ In general you should only retry on exceptions that are likely to be
118
+ transient, such as network errors.
119
+
120
+ Good exceptions to retry are all server errors (5xx) and selected client
121
+ errors (4xx) such as 429 Too Many Requests.
122
+ """
123
+
124
+ wait_exponential_jitter: bool = True
125
+ """Whether to add jitter to the exponential backoff."""
126
+
127
+ exponential_jitter_params: ExponentialJitterParams | None = None
128
+ """Parameters for `tenacity.wait_exponential_jitter`. Namely: `initial`,
129
+ `max`, `exp_base`, and `jitter` (all `float` values).
130
+ """
131
+
132
+ max_attempt_number: int = 3
133
+ """The maximum number of attempts to retry the Runnable."""
134
+
135
+ @property
136
+ def _kwargs_retrying(self) -> dict[str, Any]:
137
+ kwargs: dict[str, Any] = {}
138
+
139
+ if self.max_attempt_number:
140
+ kwargs["stop"] = stop_after_attempt(self.max_attempt_number)
141
+
142
+ if self.wait_exponential_jitter:
143
+ kwargs["wait"] = wait_exponential_jitter(
144
+ **(self.exponential_jitter_params or {})
145
+ )
146
+
147
+ if self.retry_exception_types:
148
+ kwargs["retry"] = retry_if_exception_type(self.retry_exception_types)
149
+
150
+ return kwargs
151
+
152
+ def _sync_retrying(self, **kwargs: Any) -> Retrying:
153
+ return Retrying(**self._kwargs_retrying, **kwargs)
154
+
155
+ def _async_retrying(self, **kwargs: Any) -> AsyncRetrying:
156
+ return AsyncRetrying(**self._kwargs_retrying, **kwargs)
157
+
158
+ @staticmethod
159
+ def _patch_config(
160
+ config: RunnableConfig,
161
+ run_manager: "T",
162
+ retry_state: RetryCallState,
163
+ ) -> RunnableConfig:
164
+ attempt = retry_state.attempt_number
165
+ tag = f"retry:attempt:{attempt}" if attempt > 1 else None
166
+ return patch_config(config, callbacks=run_manager.get_child(tag))
167
+
168
+ def _patch_config_list(
169
+ self,
170
+ config: list[RunnableConfig],
171
+ run_manager: list["T"],
172
+ retry_state: RetryCallState,
173
+ ) -> list[RunnableConfig]:
174
+ return [
175
+ self._patch_config(c, rm, retry_state)
176
+ for c, rm in zip(config, run_manager, strict=False)
177
+ ]
178
+
179
+ def _invoke(
180
+ self,
181
+ input_: Input,
182
+ run_manager: "CallbackManagerForChainRun",
183
+ config: RunnableConfig,
184
+ **kwargs: Any,
185
+ ) -> Output:
186
+ for attempt in self._sync_retrying(reraise=True):
187
+ with attempt:
188
+ result = super().invoke(
189
+ input_,
190
+ self._patch_config(config, run_manager, attempt.retry_state),
191
+ **kwargs,
192
+ )
193
+ if attempt.retry_state.outcome and not attempt.retry_state.outcome.failed:
194
+ attempt.retry_state.set_result(result)
195
+ return result
196
+
197
+ @override
198
+ def invoke(
199
+ self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
200
+ ) -> Output:
201
+ return self._call_with_config(self._invoke, input, config, **kwargs)
202
+
203
+ async def _ainvoke(
204
+ self,
205
+ input_: Input,
206
+ run_manager: "AsyncCallbackManagerForChainRun",
207
+ config: RunnableConfig,
208
+ **kwargs: Any,
209
+ ) -> Output:
210
+ async for attempt in self._async_retrying(reraise=True):
211
+ with attempt:
212
+ result = await super().ainvoke(
213
+ input_,
214
+ self._patch_config(config, run_manager, attempt.retry_state),
215
+ **kwargs,
216
+ )
217
+ if attempt.retry_state.outcome and not attempt.retry_state.outcome.failed:
218
+ attempt.retry_state.set_result(result)
219
+ return result
220
+
221
+ @override
222
+ async def ainvoke(
223
+ self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
224
+ ) -> Output:
225
+ return await self._acall_with_config(self._ainvoke, input, config, **kwargs)
226
+
227
+ def _batch(
228
+ self,
229
+ inputs: list[Input],
230
+ run_manager: list["CallbackManagerForChainRun"],
231
+ config: list[RunnableConfig],
232
+ **kwargs: Any,
233
+ ) -> list[Output | Exception]:
234
+ results_map: dict[int, Output] = {}
235
+
236
+ not_set: list[Output] = []
237
+ result = not_set
238
+ try:
239
+ for attempt in self._sync_retrying():
240
+ with attempt:
241
+ # Retry for inputs that have not yet succeeded
242
+ # Determine which original indices remain.
243
+ remaining_indices = [
244
+ i for i in range(len(inputs)) if i not in results_map
245
+ ]
246
+ if not remaining_indices:
247
+ break
248
+ pending_inputs = [inputs[i] for i in remaining_indices]
249
+ pending_configs = [config[i] for i in remaining_indices]
250
+ pending_run_managers = [run_manager[i] for i in remaining_indices]
251
+ # Invoke underlying batch only on remaining elements.
252
+ result = super().batch(
253
+ pending_inputs,
254
+ self._patch_config_list(
255
+ pending_configs, pending_run_managers, attempt.retry_state
256
+ ),
257
+ return_exceptions=True,
258
+ **kwargs,
259
+ )
260
+ # Register the results of the inputs that have succeeded, mapping
261
+ # back to their original indices.
262
+ first_exception = None
263
+ for offset, r in enumerate(result):
264
+ if isinstance(r, Exception):
265
+ if not first_exception:
266
+ first_exception = r
267
+ continue
268
+ orig_idx = remaining_indices[offset]
269
+ results_map[orig_idx] = r
270
+ # If any exception occurred, raise it, to retry the failed ones
271
+ if first_exception:
272
+ raise first_exception
273
+ if (
274
+ attempt.retry_state.outcome
275
+ and not attempt.retry_state.outcome.failed
276
+ ):
277
+ attempt.retry_state.set_result(result)
278
+ except RetryError as e:
279
+ if result is not_set:
280
+ result = cast("list[Output]", [e] * len(inputs))
281
+
282
+ outputs: list[Output | Exception] = []
283
+ for idx in range(len(inputs)):
284
+ if idx in results_map:
285
+ outputs.append(results_map[idx])
286
+ else:
287
+ outputs.append(result.pop(0))
288
+ return outputs
289
+
290
+ @override
291
+ def batch(
292
+ self,
293
+ inputs: list[Input],
294
+ config: RunnableConfig | list[RunnableConfig] | None = None,
295
+ *,
296
+ return_exceptions: bool = False,
297
+ **kwargs: Any,
298
+ ) -> list[Output]:
299
+ return self._batch_with_config(
300
+ self._batch, inputs, config, return_exceptions=return_exceptions, **kwargs
301
+ )
302
+
303
+ async def _abatch(
304
+ self,
305
+ inputs: list[Input],
306
+ run_manager: list["AsyncCallbackManagerForChainRun"],
307
+ config: list[RunnableConfig],
308
+ **kwargs: Any,
309
+ ) -> list[Output | Exception]:
310
+ results_map: dict[int, Output] = {}
311
+
312
+ not_set: list[Output] = []
313
+ result = not_set
314
+ try:
315
+ async for attempt in self._async_retrying():
316
+ with attempt:
317
+ # Retry for inputs that have not yet succeeded
318
+ # Determine which original indices remain.
319
+ remaining_indices = [
320
+ i for i in range(len(inputs)) if i not in results_map
321
+ ]
322
+ if not remaining_indices:
323
+ break
324
+ pending_inputs = [inputs[i] for i in remaining_indices]
325
+ pending_configs = [config[i] for i in remaining_indices]
326
+ pending_run_managers = [run_manager[i] for i in remaining_indices]
327
+ result = await super().abatch(
328
+ pending_inputs,
329
+ self._patch_config_list(
330
+ pending_configs, pending_run_managers, attempt.retry_state
331
+ ),
332
+ return_exceptions=True,
333
+ **kwargs,
334
+ )
335
+ # Register the results of the inputs that have succeeded, mapping
336
+ # back to their original indices.
337
+ first_exception = None
338
+ for offset, r in enumerate(result):
339
+ if isinstance(r, Exception):
340
+ if not first_exception:
341
+ first_exception = r
342
+ continue
343
+ orig_idx = remaining_indices[offset]
344
+ results_map[orig_idx] = r
345
+ # If any exception occurred, raise it, to retry the failed ones
346
+ if first_exception:
347
+ raise first_exception
348
+ if (
349
+ attempt.retry_state.outcome
350
+ and not attempt.retry_state.outcome.failed
351
+ ):
352
+ attempt.retry_state.set_result(result)
353
+ except RetryError as e:
354
+ if result is not_set:
355
+ result = cast("list[Output]", [e] * len(inputs))
356
+
357
+ outputs: list[Output | Exception] = []
358
+ for idx in range(len(inputs)):
359
+ if idx in results_map:
360
+ outputs.append(results_map[idx])
361
+ else:
362
+ outputs.append(result.pop(0))
363
+ return outputs
364
+
365
+ @override
366
+ async def abatch(
367
+ self,
368
+ inputs: list[Input],
369
+ config: RunnableConfig | list[RunnableConfig] | None = None,
370
+ *,
371
+ return_exceptions: bool = False,
372
+ **kwargs: Any,
373
+ ) -> list[Output]:
374
+ return await self._abatch_with_config(
375
+ self._abatch, inputs, config, return_exceptions=return_exceptions, **kwargs
376
+ )
377
+
378
+ # stream() and transform() are not retried because retrying a stream
379
+ # is not very intuitive.
@@ -0,0 +1,239 @@
1
+ """`Runnable` that routes to a set of `Runnable` objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Any,
9
+ cast,
10
+ )
11
+
12
+ from pydantic import ConfigDict
13
+ from typing_extensions import TypedDict, override
14
+
15
+ from coco_core.runnables.base import (
16
+ Runnable,
17
+ RunnableSerializable,
18
+ coerce_to_runnable,
19
+ )
20
+ from coco_core.runnables.config import (
21
+ RunnableConfig,
22
+ get_config_list,
23
+ get_executor_for_config,
24
+ )
25
+ from coco_core.runnables.utils import (
26
+ ConfigurableFieldSpec,
27
+ Input,
28
+ Output,
29
+ gather_with_concurrency,
30
+ get_unique_config_specs,
31
+ )
32
+
33
+ if TYPE_CHECKING:
34
+ from collections.abc import AsyncIterator, Callable, Iterator
35
+
36
+
37
+ class RouterInput(TypedDict):
38
+ """Router input."""
39
+
40
+ key: str
41
+ """The key to route on."""
42
+ input: Any
43
+ """The input to pass to the selected `Runnable`."""
44
+
45
+
46
+ class RouterRunnable(RunnableSerializable[RouterInput, Output]):
47
+ """`Runnable` that routes to a set of `Runnable` based on `Input['key']`.
48
+
49
+ Returns the output of the selected Runnable.
50
+
51
+ Example:
52
+ ```python
53
+ from coco_core.runnables.router import RouterRunnable
54
+ from coco_core.runnables import RunnableLambda
55
+
56
+ add = RunnableLambda(func=lambda x: x + 1)
57
+ square = RunnableLambda(func=lambda x: x**2)
58
+
59
+ router = RouterRunnable(runnables={"add": add, "square": square})
60
+ router.invoke({"key": "square", "input": 3})
61
+ ```
62
+ """
63
+
64
+ runnables: Mapping[str, Runnable[Any, Output]]
65
+
66
+ @property
67
+ @override
68
+ def config_specs(self) -> list[ConfigurableFieldSpec]:
69
+ return get_unique_config_specs(
70
+ spec for step in self.runnables.values() for spec in step.config_specs
71
+ )
72
+
73
+ def __init__(
74
+ self,
75
+ runnables: Mapping[str, Runnable[Any, Output] | Callable[[Any], Output]],
76
+ ) -> None:
77
+ """Create a `RouterRunnable`.
78
+
79
+ Args:
80
+ runnables: A mapping of keys to `Runnable` objects.
81
+ """
82
+ super().__init__(
83
+ runnables={key: coerce_to_runnable(r) for key, r in runnables.items()}
84
+ )
85
+
86
+ model_config = ConfigDict(
87
+ arbitrary_types_allowed=True,
88
+ )
89
+
90
+ @classmethod
91
+ @override
92
+ def is_lc_serializable(cls) -> bool:
93
+ """Return `True` as this class is serializable."""
94
+ return True
95
+
96
+ @classmethod
97
+ @override
98
+ def get_lc_namespace(cls) -> list[str]:
99
+ """Get the namespace of the Coco object.
100
+
101
+ Returns:
102
+ `["coco", "schema", "runnable"]`
103
+ """
104
+ return ["coco", "schema", "runnable"]
105
+
106
+ @override
107
+ def invoke(
108
+ self, input: RouterInput, config: RunnableConfig | None = None, **kwargs: Any
109
+ ) -> Output:
110
+ key = input["key"]
111
+ actual_input = input["input"]
112
+ if key not in self.runnables:
113
+ msg = f"No runnable associated with key '{key}'"
114
+ raise ValueError(msg)
115
+
116
+ runnable = self.runnables[key]
117
+ return runnable.invoke(actual_input, config)
118
+
119
+ @override
120
+ async def ainvoke(
121
+ self,
122
+ input: RouterInput,
123
+ config: RunnableConfig | None = None,
124
+ **kwargs: Any | None,
125
+ ) -> Output:
126
+ key = input["key"]
127
+ actual_input = input["input"]
128
+ if key not in self.runnables:
129
+ msg = f"No runnable associated with key '{key}'"
130
+ raise ValueError(msg)
131
+
132
+ runnable = self.runnables[key]
133
+ return await runnable.ainvoke(actual_input, config)
134
+
135
+ @override
136
+ def batch(
137
+ self,
138
+ inputs: list[RouterInput],
139
+ config: RunnableConfig | list[RunnableConfig] | None = None,
140
+ *,
141
+ return_exceptions: bool = False,
142
+ **kwargs: Any | None,
143
+ ) -> list[Output]:
144
+ if not inputs:
145
+ return []
146
+
147
+ keys = [input_["key"] for input_ in inputs]
148
+ actual_inputs = [input_["input"] for input_ in inputs]
149
+ if any(key not in self.runnables for key in keys):
150
+ msg = "One or more keys do not have a corresponding runnable"
151
+ raise ValueError(msg)
152
+
153
+ def invoke(
154
+ runnable: Runnable[Input, Output], input_: Input, config: RunnableConfig
155
+ ) -> Output | Exception:
156
+ if return_exceptions:
157
+ try:
158
+ return runnable.invoke(input_, config, **kwargs)
159
+ except Exception as e:
160
+ return e
161
+ else:
162
+ return runnable.invoke(input_, config, **kwargs)
163
+
164
+ runnables = [self.runnables[key] for key in keys]
165
+ configs = get_config_list(config, len(inputs))
166
+ with get_executor_for_config(configs[0]) as executor:
167
+ return cast(
168
+ "list[Output]",
169
+ list(executor.map(invoke, runnables, actual_inputs, configs)),
170
+ )
171
+
172
+ @override
173
+ async def abatch(
174
+ self,
175
+ inputs: list[RouterInput],
176
+ config: RunnableConfig | list[RunnableConfig] | None = None,
177
+ *,
178
+ return_exceptions: bool = False,
179
+ **kwargs: Any | None,
180
+ ) -> list[Output]:
181
+ if not inputs:
182
+ return []
183
+
184
+ keys = [input_["key"] for input_ in inputs]
185
+ actual_inputs = [input_["input"] for input_ in inputs]
186
+ if any(key not in self.runnables for key in keys):
187
+ msg = "One or more keys do not have a corresponding runnable"
188
+ raise ValueError(msg)
189
+
190
+ async def ainvoke(
191
+ runnable: Runnable[Input, Output], input_: Input, config: RunnableConfig
192
+ ) -> Output | Exception:
193
+ if return_exceptions:
194
+ try:
195
+ return await runnable.ainvoke(input_, config, **kwargs)
196
+ except Exception as e:
197
+ return e
198
+ else:
199
+ return await runnable.ainvoke(input_, config, **kwargs)
200
+
201
+ runnables = [self.runnables[key] for key in keys]
202
+ configs = get_config_list(config, len(inputs))
203
+ return await gather_with_concurrency(
204
+ configs[0].get("max_concurrency"),
205
+ *map(ainvoke, runnables, actual_inputs, configs),
206
+ )
207
+
208
+ @override
209
+ def stream(
210
+ self,
211
+ input: RouterInput,
212
+ config: RunnableConfig | None = None,
213
+ **kwargs: Any | None,
214
+ ) -> Iterator[Output]:
215
+ key = input["key"]
216
+ actual_input = input["input"]
217
+ if key not in self.runnables:
218
+ msg = f"No runnable associated with key '{key}'"
219
+ raise ValueError(msg)
220
+
221
+ runnable = self.runnables[key]
222
+ yield from runnable.stream(actual_input, config)
223
+
224
+ @override
225
+ async def astream(
226
+ self,
227
+ input: RouterInput,
228
+ config: RunnableConfig | None = None,
229
+ **kwargs: Any | None,
230
+ ) -> AsyncIterator[Output]:
231
+ key = input["key"]
232
+ actual_input = input["input"]
233
+ if key not in self.runnables:
234
+ msg = f"No runnable associated with key '{key}'"
235
+ raise ValueError(msg)
236
+
237
+ runnable = self.runnables[key]
238
+ async for output in runnable.astream(actual_input, config):
239
+ yield output