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,663 @@
1
+ """`Runnable` that can fallback to other `Runnable` objects if it fails."""
2
+
3
+ import asyncio
4
+ import inspect
5
+ import typing
6
+ from collections.abc import AsyncIterator, Iterator, Sequence
7
+ from functools import wraps
8
+ from typing import TYPE_CHECKING, Any, cast
9
+
10
+ from pydantic import ConfigDict
11
+ from typing_extensions import override
12
+
13
+ from coco_core.callbacks.manager import AsyncCallbackManager, CallbackManager
14
+ from coco_core.runnables.base import Runnable, RunnableSerializable
15
+ from coco_core.runnables.config import (
16
+ RunnableConfig,
17
+ ensure_config,
18
+ get_async_callback_manager_for_config,
19
+ get_callback_manager_for_config,
20
+ get_config_list,
21
+ patch_config,
22
+ set_config_context,
23
+ )
24
+ from coco_core.runnables.utils import (
25
+ ConfigurableFieldSpec,
26
+ Input,
27
+ Output,
28
+ coro_with_context,
29
+ get_unique_config_specs,
30
+ )
31
+ from coco_core.utils.pydantic import TypeBaseModel
32
+
33
+ if TYPE_CHECKING:
34
+ from coco_core.callbacks.manager import AsyncCallbackManagerForChainRun
35
+
36
+
37
+ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
38
+ """`Runnable` that can fallback to other `Runnable` objects if it fails.
39
+
40
+ External APIs (e.g., APIs for a language model) may at times experience
41
+ degraded performance or even downtime.
42
+
43
+ In these cases, it can be useful to have a fallback `Runnable` that can be
44
+ used in place of the original `Runnable` (e.g., fallback to another LLM provider).
45
+
46
+ Fallbacks can be defined at the level of a single `Runnable`, or at the level
47
+ of a chain of `Runnable`s. Fallbacks are tried in order until one succeeds or
48
+ all fail.
49
+
50
+ While you can instantiate a `RunnableWithFallbacks` directly, it is usually
51
+ more convenient to use the `with_fallbacks` method on a `Runnable`.
52
+
53
+ Example:
54
+ ```python
55
+ from coco_anthropic import ChatAnthropic
56
+ from coco_openai import ChatOpenAI
57
+
58
+ model = ChatAnthropic(model="claude-sonnet-5").with_fallbacks(
59
+ [ChatOpenAI(model="gpt-5.4-mini")]
60
+ )
61
+ # Will usually use ChatAnthropic, but fallback to ChatOpenAI
62
+ # if ChatAnthropic fails.
63
+ model.invoke("hello")
64
+
65
+ # And you can also use fallbacks at the level of a chain.
66
+ # Here if both LLM providers fail, we'll fallback to a good hardcoded
67
+ # response.
68
+
69
+ from coco_core.prompts import PromptTemplate
70
+ from coco_core.output_parser import StrOutputParser
71
+ from coco_core.runnables import RunnableLambda
72
+
73
+
74
+ def when_all_is_lost(inputs):
75
+ return (
76
+ "Looks like our LLM providers are down. "
77
+ "Here's a nice 🦜️ emoji for you instead."
78
+ )
79
+
80
+
81
+ chain_with_fallback = (
82
+ PromptTemplate.from_template("Tell me a joke about {topic}")
83
+ | model
84
+ | StrOutputParser()
85
+ ).with_fallbacks([RunnableLambda(when_all_is_lost)])
86
+ ```
87
+ """
88
+
89
+ runnable: Runnable[Input, Output]
90
+ """The `Runnable` to run first."""
91
+ fallbacks: Sequence[Runnable[Input, Output]]
92
+ """A sequence of fallbacks to try."""
93
+ exceptions_to_handle: tuple[type[BaseException], ...] = (Exception,)
94
+ """The exceptions on which fallbacks should be tried.
95
+
96
+ Any exception that is not a subclass of these exceptions will be raised immediately.
97
+ """
98
+ exception_key: str | None = None
99
+ """If `string` is specified then handled exceptions will be passed to fallbacks as
100
+ part of the input under the specified key.
101
+
102
+ If `None`, exceptions will not be passed to fallbacks.
103
+
104
+ If used, the base `Runnable` and its fallbacks must accept a dictionary as input.
105
+ """
106
+
107
+ model_config = ConfigDict(
108
+ arbitrary_types_allowed=True,
109
+ )
110
+
111
+ @property
112
+ @override
113
+ def InputType(self) -> type[Input]:
114
+ return self.runnable.InputType
115
+
116
+ @property
117
+ @override
118
+ def OutputType(self) -> type[Output]:
119
+ return self.runnable.OutputType
120
+
121
+ @override
122
+ def get_input_schema(self, config: RunnableConfig | None = None) -> TypeBaseModel:
123
+ return self.runnable.get_input_schema(config)
124
+
125
+ @override
126
+ def get_output_schema(self, config: RunnableConfig | None = None) -> TypeBaseModel:
127
+ return self.runnable.get_output_schema(config)
128
+
129
+ @property
130
+ @override
131
+ def config_specs(self) -> list[ConfigurableFieldSpec]:
132
+ return get_unique_config_specs(
133
+ spec
134
+ for step in [self.runnable, *self.fallbacks]
135
+ for spec in step.config_specs
136
+ )
137
+
138
+ @classmethod
139
+ @override
140
+ def is_lc_serializable(cls) -> bool:
141
+ """Return `True` as this class is serializable."""
142
+ return True
143
+
144
+ @classmethod
145
+ @override
146
+ def get_lc_namespace(cls) -> list[str]:
147
+ """Get the namespace of the Coco object.
148
+
149
+ Returns:
150
+ `["coco", "schema", "runnable"]`
151
+ """
152
+ return ["coco", "schema", "runnable"]
153
+
154
+ @property
155
+ def runnables(self) -> Iterator[Runnable[Input, Output]]:
156
+ """Iterator over the `Runnable` and its fallbacks.
157
+
158
+ Yields:
159
+ The `Runnable` then its fallbacks.
160
+ """
161
+ yield self.runnable
162
+ yield from self.fallbacks
163
+
164
+ @override
165
+ def invoke(
166
+ self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
167
+ ) -> Output:
168
+ if self.exception_key is not None and not isinstance(input, dict):
169
+ msg = (
170
+ "If 'exception_key' is specified then input must be a dictionary."
171
+ f"However found a type of {type(input)} for input"
172
+ )
173
+ raise ValueError(msg)
174
+ # setup callbacks
175
+ config = ensure_config(config)
176
+ callback_manager = get_callback_manager_for_config(config)
177
+ # start the root run
178
+ run_manager = callback_manager.on_chain_start(
179
+ None,
180
+ input,
181
+ name=config.get("run_name") or self.get_name(),
182
+ run_id=config.pop("run_id", None),
183
+ )
184
+ first_error = None
185
+ last_error = None
186
+ for runnable in self.runnables:
187
+ try:
188
+ if self.exception_key and last_error is not None:
189
+ input[self.exception_key] = last_error # type: ignore[index]
190
+ child_config = patch_config(config, callbacks=run_manager.get_child())
191
+ with set_config_context(child_config) as context:
192
+ output = context.run(
193
+ runnable.invoke,
194
+ input,
195
+ config,
196
+ **kwargs,
197
+ )
198
+ except self.exceptions_to_handle as e:
199
+ if first_error is None:
200
+ first_error = e
201
+ last_error = e
202
+ except BaseException as e:
203
+ run_manager.on_chain_error(e)
204
+ raise
205
+ else:
206
+ run_manager.on_chain_end(output)
207
+ return output
208
+ if first_error is None:
209
+ msg = "No error stored at end of fallbacks."
210
+ raise ValueError(msg)
211
+ run_manager.on_chain_error(first_error)
212
+ raise first_error
213
+
214
+ @override
215
+ async def ainvoke(
216
+ self,
217
+ input: Input,
218
+ config: RunnableConfig | None = None,
219
+ **kwargs: Any | None,
220
+ ) -> Output:
221
+ if self.exception_key is not None and not isinstance(input, dict):
222
+ msg = (
223
+ "If 'exception_key' is specified then input must be a dictionary."
224
+ f"However found a type of {type(input)} for input"
225
+ )
226
+ raise ValueError(msg)
227
+ # setup callbacks
228
+ config = ensure_config(config)
229
+ callback_manager = get_async_callback_manager_for_config(config)
230
+ # start the root run
231
+ run_manager = await callback_manager.on_chain_start(
232
+ None,
233
+ input,
234
+ name=config.get("run_name") or self.get_name(),
235
+ run_id=config.pop("run_id", None),
236
+ )
237
+
238
+ first_error = None
239
+ last_error = None
240
+ for runnable in self.runnables:
241
+ try:
242
+ if self.exception_key and last_error is not None:
243
+ input[self.exception_key] = last_error # type: ignore[index]
244
+ child_config = patch_config(config, callbacks=run_manager.get_child())
245
+ with set_config_context(child_config) as context:
246
+ coro = context.run(runnable.ainvoke, input, config, **kwargs)
247
+ output = await coro_with_context(coro, context)
248
+ except self.exceptions_to_handle as e:
249
+ if first_error is None:
250
+ first_error = e
251
+ last_error = e
252
+ except BaseException as e:
253
+ await run_manager.on_chain_error(e)
254
+ raise
255
+ else:
256
+ await run_manager.on_chain_end(output)
257
+ return output
258
+ if first_error is None:
259
+ msg = "No error stored at end of fallbacks."
260
+ raise ValueError(msg)
261
+ await run_manager.on_chain_error(first_error)
262
+ raise first_error
263
+
264
+ @override
265
+ def batch(
266
+ self,
267
+ inputs: list[Input],
268
+ config: RunnableConfig | list[RunnableConfig] | None = None,
269
+ *,
270
+ return_exceptions: bool = False,
271
+ **kwargs: Any | None,
272
+ ) -> list[Output]:
273
+ if self.exception_key is not None and not all(
274
+ isinstance(input_, dict) for input_ in inputs
275
+ ):
276
+ msg = (
277
+ "If 'exception_key' is specified then inputs must be dictionaries."
278
+ f"However found a type of {type(inputs[0])} for input"
279
+ )
280
+ raise ValueError(msg)
281
+
282
+ if not inputs:
283
+ return []
284
+
285
+ # setup callbacks
286
+ configs = get_config_list(config, len(inputs))
287
+ callback_managers = [
288
+ CallbackManager.configure(
289
+ inheritable_callbacks=config.get("callbacks"),
290
+ local_callbacks=None,
291
+ verbose=False,
292
+ inheritable_tags=config.get("tags"),
293
+ local_tags=None,
294
+ inheritable_metadata=config.get("metadata"),
295
+ local_metadata=None,
296
+ )
297
+ for config in configs
298
+ ]
299
+ # start the root runs, one per input
300
+ run_managers = [
301
+ cm.on_chain_start(
302
+ None,
303
+ input_ if isinstance(input_, dict) else {"input": input_},
304
+ name=config.get("run_name") or self.get_name(),
305
+ run_id=config.pop("run_id", None),
306
+ )
307
+ for cm, input_, config in zip(
308
+ callback_managers, inputs, configs, strict=False
309
+ )
310
+ ]
311
+
312
+ to_return: dict[int, Any] = {}
313
+ run_again = dict(enumerate(inputs))
314
+ handled_exceptions: dict[int, BaseException] = {}
315
+ first_to_raise = None
316
+ for runnable in self.runnables:
317
+ outputs = runnable.batch(
318
+ [input_ for _, input_ in sorted(run_again.items())],
319
+ [
320
+ # each step a child run of the corresponding root run
321
+ patch_config(configs[i], callbacks=run_managers[i].get_child())
322
+ for i in sorted(run_again)
323
+ ],
324
+ return_exceptions=True,
325
+ **kwargs,
326
+ )
327
+ for (i, input_), output in zip(
328
+ sorted(run_again.copy().items()), outputs, strict=False
329
+ ):
330
+ if isinstance(output, BaseException) and not isinstance(
331
+ output, self.exceptions_to_handle
332
+ ):
333
+ if not return_exceptions:
334
+ first_to_raise = first_to_raise or output
335
+ else:
336
+ handled_exceptions[i] = output
337
+ run_again.pop(i)
338
+ elif isinstance(output, self.exceptions_to_handle):
339
+ if self.exception_key:
340
+ input_[self.exception_key] = output # type: ignore[index]
341
+ handled_exceptions[i] = output
342
+ else:
343
+ run_managers[i].on_chain_end(output)
344
+ to_return[i] = output
345
+ run_again.pop(i)
346
+ handled_exceptions.pop(i, None)
347
+ if first_to_raise:
348
+ raise first_to_raise
349
+ if not run_again:
350
+ break
351
+
352
+ sorted_handled_exceptions = sorted(handled_exceptions.items())
353
+ for i, error in sorted_handled_exceptions:
354
+ run_managers[i].on_chain_error(error)
355
+ if not return_exceptions and sorted_handled_exceptions:
356
+ raise sorted_handled_exceptions[0][1]
357
+ to_return.update(handled_exceptions)
358
+ return [output for _, output in sorted(to_return.items())]
359
+
360
+ @override
361
+ async def abatch(
362
+ self,
363
+ inputs: list[Input],
364
+ config: RunnableConfig | list[RunnableConfig] | None = None,
365
+ *,
366
+ return_exceptions: bool = False,
367
+ **kwargs: Any | None,
368
+ ) -> list[Output]:
369
+ if self.exception_key is not None and not all(
370
+ isinstance(input_, dict) for input_ in inputs
371
+ ):
372
+ msg = (
373
+ "If 'exception_key' is specified then inputs must be dictionaries."
374
+ f"However found a type of {type(inputs[0])} for input"
375
+ )
376
+ raise ValueError(msg)
377
+
378
+ if not inputs:
379
+ return []
380
+
381
+ # setup callbacks
382
+ configs = get_config_list(config, len(inputs))
383
+ callback_managers = [
384
+ AsyncCallbackManager.configure(
385
+ inheritable_callbacks=config.get("callbacks"),
386
+ local_callbacks=None,
387
+ verbose=False,
388
+ inheritable_tags=config.get("tags"),
389
+ local_tags=None,
390
+ inheritable_metadata=config.get("metadata"),
391
+ local_metadata=None,
392
+ )
393
+ for config in configs
394
+ ]
395
+ # start the root runs, one per input
396
+ run_managers: list[AsyncCallbackManagerForChainRun] = await asyncio.gather(
397
+ *(
398
+ cm.on_chain_start(
399
+ None,
400
+ input_,
401
+ name=config.get("run_name") or self.get_name(),
402
+ run_id=config.pop("run_id", None),
403
+ )
404
+ for cm, input_, config in zip(
405
+ callback_managers, inputs, configs, strict=False
406
+ )
407
+ )
408
+ )
409
+
410
+ to_return: dict[int, Output | BaseException] = {}
411
+ run_again = dict(enumerate(inputs))
412
+ handled_exceptions: dict[int, BaseException] = {}
413
+ first_to_raise = None
414
+ for runnable in self.runnables:
415
+ outputs = await runnable.abatch(
416
+ [input_ for _, input_ in sorted(run_again.items())],
417
+ [
418
+ # each step a child run of the corresponding root run
419
+ patch_config(configs[i], callbacks=run_managers[i].get_child())
420
+ for i in sorted(run_again)
421
+ ],
422
+ return_exceptions=True,
423
+ **kwargs,
424
+ )
425
+
426
+ for (i, input_), output in zip(
427
+ sorted(run_again.copy().items()), outputs, strict=False
428
+ ):
429
+ if isinstance(output, BaseException) and not isinstance(
430
+ output, self.exceptions_to_handle
431
+ ):
432
+ if not return_exceptions:
433
+ first_to_raise = first_to_raise or output
434
+ else:
435
+ handled_exceptions[i] = output
436
+ run_again.pop(i)
437
+ elif isinstance(output, self.exceptions_to_handle):
438
+ if self.exception_key:
439
+ input_[self.exception_key] = output # type: ignore[index]
440
+ handled_exceptions[i] = output
441
+ else:
442
+ to_return[i] = output
443
+ await run_managers[i].on_chain_end(output)
444
+ run_again.pop(i)
445
+ handled_exceptions.pop(i, None)
446
+
447
+ if first_to_raise:
448
+ raise first_to_raise
449
+ if not run_again:
450
+ break
451
+
452
+ sorted_handled_exceptions = sorted(handled_exceptions.items())
453
+ await asyncio.gather(
454
+ *(
455
+ run_managers[i].on_chain_error(error)
456
+ for i, error in sorted_handled_exceptions
457
+ )
458
+ )
459
+ if not return_exceptions and sorted_handled_exceptions:
460
+ raise sorted_handled_exceptions[0][1]
461
+ to_return.update(handled_exceptions)
462
+ return [cast("Output", output) for _, output in sorted(to_return.items())]
463
+
464
+ @override
465
+ def stream(
466
+ self,
467
+ input: Input,
468
+ config: RunnableConfig | None = None,
469
+ **kwargs: Any | None,
470
+ ) -> Iterator[Output]:
471
+ if self.exception_key is not None and not isinstance(input, dict):
472
+ msg = (
473
+ "If 'exception_key' is specified then input must be a dictionary."
474
+ f"However found a type of {type(input)} for input"
475
+ )
476
+ raise ValueError(msg)
477
+ # setup callbacks
478
+ config = ensure_config(config)
479
+ callback_manager = get_callback_manager_for_config(config)
480
+ # start the root run
481
+ run_manager = callback_manager.on_chain_start(
482
+ None,
483
+ input,
484
+ name=config.get("run_name") or self.get_name(),
485
+ run_id=config.pop("run_id", None),
486
+ )
487
+ first_error = None
488
+ last_error = None
489
+ for runnable in self.runnables:
490
+ try:
491
+ if self.exception_key and last_error is not None:
492
+ input[self.exception_key] = last_error # type: ignore[index]
493
+ child_config = patch_config(config, callbacks=run_manager.get_child())
494
+ with set_config_context(child_config) as context:
495
+ stream = context.run(
496
+ runnable.stream,
497
+ input,
498
+ **kwargs,
499
+ )
500
+ chunk: Output = context.run(next, stream)
501
+ except self.exceptions_to_handle as e:
502
+ first_error = e if first_error is None else first_error
503
+ last_error = e
504
+ except BaseException as e:
505
+ run_manager.on_chain_error(e)
506
+ raise
507
+ else:
508
+ first_error = None
509
+ break
510
+ if first_error:
511
+ run_manager.on_chain_error(first_error)
512
+ raise first_error
513
+
514
+ yield chunk
515
+ output: Output | None = chunk
516
+ try:
517
+ for chunk in stream:
518
+ yield chunk
519
+ try:
520
+ output = output + chunk # type: ignore[operator]
521
+ except TypeError:
522
+ output = None
523
+ except BaseException as e:
524
+ run_manager.on_chain_error(e)
525
+ raise
526
+ run_manager.on_chain_end(output)
527
+
528
+ @override
529
+ async def astream(
530
+ self,
531
+ input: Input,
532
+ config: RunnableConfig | None = None,
533
+ **kwargs: Any | None,
534
+ ) -> AsyncIterator[Output]:
535
+ if self.exception_key is not None and not isinstance(input, dict):
536
+ msg = (
537
+ "If 'exception_key' is specified then input must be a dictionary."
538
+ f"However found a type of {type(input)} for input"
539
+ )
540
+ raise ValueError(msg)
541
+ # setup callbacks
542
+ config = ensure_config(config)
543
+ callback_manager = get_async_callback_manager_for_config(config)
544
+ # start the root run
545
+ run_manager = await callback_manager.on_chain_start(
546
+ None,
547
+ input,
548
+ name=config.get("run_name") or self.get_name(),
549
+ run_id=config.pop("run_id", None),
550
+ )
551
+ first_error = None
552
+ last_error = None
553
+ for runnable in self.runnables:
554
+ try:
555
+ if self.exception_key and last_error is not None:
556
+ input[self.exception_key] = last_error # type: ignore[index]
557
+ child_config = patch_config(config, callbacks=run_manager.get_child())
558
+ with set_config_context(child_config) as context:
559
+ stream = runnable.astream(
560
+ input,
561
+ child_config,
562
+ **kwargs,
563
+ )
564
+ chunk = await coro_with_context(anext(stream), context)
565
+ except self.exceptions_to_handle as e:
566
+ first_error = e if first_error is None else first_error
567
+ last_error = e
568
+ except BaseException as e:
569
+ await run_manager.on_chain_error(e)
570
+ raise
571
+ else:
572
+ first_error = None
573
+ break
574
+ if first_error:
575
+ await run_manager.on_chain_error(first_error)
576
+ raise first_error
577
+
578
+ yield chunk
579
+ output: Output | None = chunk
580
+ try:
581
+ async for chunk in stream:
582
+ yield chunk
583
+ try:
584
+ output = output + chunk # type: ignore[operator]
585
+ except TypeError:
586
+ output = None
587
+ except BaseException as e:
588
+ await run_manager.on_chain_error(e)
589
+ raise
590
+ await run_manager.on_chain_end(output)
591
+
592
+ def __getattr__(self, name: str) -> Any:
593
+ """Get an attribute from the wrapped `Runnable` and its fallbacks.
594
+
595
+ Returns:
596
+ If the attribute is anything other than a method that outputs a `Runnable`,
597
+ returns `getattr(self.runnable, name)`. If the attribute is a method that
598
+ does return a new `Runnable` (e.g. `model.bind_tools([...])` outputs a new
599
+ `RunnableBinding`) then `self.runnable` and each of the runnables in
600
+ `self.fallbacks` is replaced with `getattr(x, name)`.
601
+
602
+ Example:
603
+ ```python
604
+ from coco_openai import ChatOpenAI
605
+ from coco_anthropic import ChatAnthropic
606
+
607
+ gpt_55 = ChatOpenAI(model="gpt-5.5")
608
+ claude_3_sonnet = ChatAnthropic(model="claude-sonnet-5")
609
+ model = gpt_55.with_fallbacks([claude_3_sonnet])
610
+
611
+ model.model_name
612
+ # -> "gpt-5.5"
613
+
614
+ # .bind_tools() is called on both ChatOpenAI and ChatAnthropic
615
+ # Equivalent to:
616
+ # gpt_55.bind_tools([...]).with_fallbacks([claude_3_sonnet.bind_tools([...])])
617
+ model.bind_tools([...])
618
+ # -> RunnableWithFallbacks(
619
+ runnable=RunnableBinding(bound=ChatOpenAI(...), kwargs={"tools": [...]}),
620
+ fallbacks=[RunnableBinding(bound=ChatAnthropic(...), kwargs={"tools": [...]})],
621
+ )
622
+ ```
623
+ """ # noqa: E501
624
+ attr = getattr(self.runnable, name)
625
+ if _returns_runnable(attr):
626
+
627
+ @wraps(attr)
628
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
629
+ new_runnable = attr(*args, **kwargs)
630
+ new_fallbacks = []
631
+ for fallback in self.fallbacks:
632
+ fallback_attr = getattr(fallback, name)
633
+ new_fallbacks.append(fallback_attr(*args, **kwargs))
634
+
635
+ return self.__class__(
636
+ **{
637
+ **self.model_dump(),
638
+ "runnable": new_runnable,
639
+ "fallbacks": new_fallbacks,
640
+ }
641
+ )
642
+
643
+ return wrapped
644
+
645
+ return attr
646
+
647
+
648
+ def _returns_runnable(attr: Any) -> bool:
649
+ if not callable(attr):
650
+ return False
651
+ return_type = typing.get_type_hints(attr).get("return")
652
+ return bool(return_type and _is_runnable_type(return_type))
653
+
654
+
655
+ def _is_runnable_type(type_: Any) -> bool:
656
+ if inspect.isclass(type_):
657
+ return issubclass(type_, Runnable)
658
+ origin = getattr(type_, "__origin__", None)
659
+ if inspect.isclass(origin):
660
+ return issubclass(origin, Runnable)
661
+ if origin is typing.Union:
662
+ return all(_is_runnable_type(t) for t in type_.__args__)
663
+ return False