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,710 @@
1
+ """`Runnable` objects that can be dynamically configured."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ import threading
7
+ from abc import abstractmethod
8
+ from collections.abc import (
9
+ AsyncIterator,
10
+ Callable,
11
+ Iterator,
12
+ Sequence,
13
+ )
14
+ from functools import wraps
15
+ from typing import (
16
+ TYPE_CHECKING,
17
+ Any,
18
+ cast,
19
+ )
20
+ from weakref import WeakValueDictionary
21
+
22
+ from pydantic import ConfigDict
23
+ from typing_extensions import override
24
+
25
+ from coco_core.runnables.base import Runnable, RunnableSerializable
26
+ from coco_core.runnables.config import (
27
+ RunnableConfig,
28
+ ensure_config,
29
+ get_config_list,
30
+ get_executor_for_config,
31
+ merge_configs,
32
+ )
33
+ from coco_core.runnables.utils import (
34
+ AnyConfigurableField,
35
+ ConfigurableField,
36
+ ConfigurableFieldMultiOption,
37
+ ConfigurableFieldSingleOption,
38
+ ConfigurableFieldSpec,
39
+ Input,
40
+ Output,
41
+ gather_with_concurrency,
42
+ get_unique_config_specs,
43
+ )
44
+
45
+ if TYPE_CHECKING:
46
+ from coco_core.runnables.graph import Graph
47
+ from coco_core.utils.pydantic import TypeBaseModel
48
+
49
+
50
+ class DynamicRunnable(RunnableSerializable[Input, Output]):
51
+ """Serializable `Runnable` that can be dynamically configured.
52
+
53
+ A `DynamicRunnable` should be initiated using the `configurable_fields` or
54
+ `configurable_alternatives` method of a `Runnable`.
55
+ """
56
+
57
+ default: RunnableSerializable[Input, Output]
58
+ """The default `Runnable` to use."""
59
+
60
+ config: RunnableConfig | None = None
61
+ """The configuration to use."""
62
+
63
+ model_config = ConfigDict(
64
+ arbitrary_types_allowed=True,
65
+ )
66
+
67
+ @classmethod
68
+ @override
69
+ def is_lc_serializable(cls) -> bool:
70
+ """Return `True` as this class is serializable."""
71
+ return True
72
+
73
+ @classmethod
74
+ @override
75
+ def get_lc_namespace(cls) -> list[str]:
76
+ """Get the namespace of the Coco object.
77
+
78
+ Returns:
79
+ `["coco", "schema", "runnable"]`
80
+ """
81
+ return ["coco", "schema", "runnable"]
82
+
83
+ @property
84
+ @override
85
+ def InputType(self) -> type[Input]:
86
+ return self.default.InputType
87
+
88
+ @property
89
+ @override
90
+ def OutputType(self) -> type[Output]:
91
+ return self.default.OutputType
92
+
93
+ @override
94
+ def get_input_schema(self, config: RunnableConfig | None = None) -> TypeBaseModel:
95
+ runnable, config = self.prepare(config)
96
+ return runnable.get_input_schema(config)
97
+
98
+ @override
99
+ def get_output_schema(self, config: RunnableConfig | None = None) -> TypeBaseModel:
100
+ runnable, config = self.prepare(config)
101
+ return runnable.get_output_schema(config)
102
+
103
+ @override
104
+ def get_graph(self, config: RunnableConfig | None = None) -> Graph:
105
+ runnable, config = self.prepare(config)
106
+ return runnable.get_graph(config)
107
+
108
+ @override
109
+ def with_config(
110
+ self,
111
+ config: RunnableConfig | None = None,
112
+ # Sadly Unpack is not well supported by mypy so this will have to be untyped
113
+ **kwargs: Any,
114
+ ) -> Runnable[Input, Output]:
115
+ return self.__class__(
116
+ **{**self.__dict__, "config": ensure_config(merge_configs(config, kwargs))} # type: ignore[arg-type]
117
+ )
118
+
119
+ def prepare(
120
+ self, config: RunnableConfig | None = None
121
+ ) -> tuple[Runnable[Input, Output], RunnableConfig]:
122
+ """Prepare the `Runnable` for invocation.
123
+
124
+ Args:
125
+ config: The configuration to use.
126
+
127
+ Returns:
128
+ The prepared `Runnable` and configuration.
129
+ """
130
+ runnable: Runnable[Input, Output] = self
131
+ while isinstance(runnable, DynamicRunnable):
132
+ runnable, config = runnable._prepare(merge_configs(runnable.config, config)) # noqa: SLF001
133
+ return runnable, cast("RunnableConfig", config)
134
+
135
+ @abstractmethod
136
+ def _prepare(
137
+ self, config: RunnableConfig | None = None
138
+ ) -> tuple[Runnable[Input, Output], RunnableConfig]: ...
139
+
140
+ @override
141
+ def invoke(
142
+ self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
143
+ ) -> Output:
144
+ runnable, config = self.prepare(config)
145
+ return runnable.invoke(input, config, **kwargs)
146
+
147
+ @override
148
+ async def ainvoke(
149
+ self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
150
+ ) -> Output:
151
+ runnable, config = self.prepare(config)
152
+ return await runnable.ainvoke(input, config, **kwargs)
153
+
154
+ @override
155
+ def batch(
156
+ self,
157
+ inputs: list[Input],
158
+ config: RunnableConfig | list[RunnableConfig] | None = None,
159
+ *,
160
+ return_exceptions: bool = False,
161
+ **kwargs: Any | None,
162
+ ) -> list[Output]:
163
+ configs = get_config_list(config, len(inputs))
164
+ prepared = [self.prepare(c) for c in configs]
165
+
166
+ if all(p is self.default for p, _ in prepared):
167
+ return self.default.batch(
168
+ inputs,
169
+ [c for _, c in prepared],
170
+ return_exceptions=return_exceptions,
171
+ **kwargs,
172
+ )
173
+
174
+ if not inputs:
175
+ return []
176
+
177
+ def invoke(
178
+ prepared: tuple[Runnable[Input, Output], RunnableConfig],
179
+ input_: Input,
180
+ ) -> Output | Exception:
181
+ bound, config = prepared
182
+ if return_exceptions:
183
+ try:
184
+ return bound.invoke(input_, config, **kwargs)
185
+ except Exception as e:
186
+ return e
187
+ else:
188
+ return bound.invoke(input_, config, **kwargs)
189
+
190
+ # If there's only one input, don't bother with the executor
191
+ if len(inputs) == 1:
192
+ return cast("list[Output]", [invoke(prepared[0], inputs[0])])
193
+
194
+ with get_executor_for_config(configs[0]) as executor:
195
+ return cast("list[Output]", list(executor.map(invoke, prepared, inputs)))
196
+
197
+ @override
198
+ async def abatch(
199
+ self,
200
+ inputs: list[Input],
201
+ config: RunnableConfig | list[RunnableConfig] | None = None,
202
+ *,
203
+ return_exceptions: bool = False,
204
+ **kwargs: Any | None,
205
+ ) -> list[Output]:
206
+ configs = get_config_list(config, len(inputs))
207
+ prepared = [self.prepare(c) for c in configs]
208
+
209
+ if all(p is self.default for p, _ in prepared):
210
+ return await self.default.abatch(
211
+ inputs,
212
+ [c for _, c in prepared],
213
+ return_exceptions=return_exceptions,
214
+ **kwargs,
215
+ )
216
+
217
+ if not inputs:
218
+ return []
219
+
220
+ async def ainvoke(
221
+ prepared: tuple[Runnable[Input, Output], RunnableConfig],
222
+ input_: Input,
223
+ ) -> Output | Exception:
224
+ bound, config = prepared
225
+ if return_exceptions:
226
+ try:
227
+ return await bound.ainvoke(input_, config, **kwargs)
228
+ except Exception as e:
229
+ return e
230
+ else:
231
+ return await bound.ainvoke(input_, config, **kwargs)
232
+
233
+ coros = map(ainvoke, prepared, inputs)
234
+ return await gather_with_concurrency(configs[0].get("max_concurrency"), *coros)
235
+
236
+ @override
237
+ def stream(
238
+ self,
239
+ input: Input,
240
+ config: RunnableConfig | None = None,
241
+ **kwargs: Any | None,
242
+ ) -> Iterator[Output]:
243
+ runnable, config = self.prepare(config)
244
+ return runnable.stream(input, config, **kwargs)
245
+
246
+ @override
247
+ async def astream(
248
+ self,
249
+ input: Input,
250
+ config: RunnableConfig | None = None,
251
+ **kwargs: Any | None,
252
+ ) -> AsyncIterator[Output]:
253
+ runnable, config = self.prepare(config)
254
+ async for chunk in runnable.astream(input, config, **kwargs):
255
+ yield chunk
256
+
257
+ @override
258
+ def transform(
259
+ self,
260
+ input: Iterator[Input],
261
+ config: RunnableConfig | None = None,
262
+ **kwargs: Any | None,
263
+ ) -> Iterator[Output]:
264
+ runnable, config = self.prepare(config)
265
+ return runnable.transform(input, config, **kwargs)
266
+
267
+ @override
268
+ async def atransform(
269
+ self,
270
+ input: AsyncIterator[Input],
271
+ config: RunnableConfig | None = None,
272
+ **kwargs: Any | None,
273
+ ) -> AsyncIterator[Output]:
274
+ runnable, config = self.prepare(config)
275
+ async for chunk in runnable.atransform(input, config, **kwargs):
276
+ yield chunk
277
+
278
+ @override
279
+ def __getattr__(self, name: str) -> Any: # type: ignore[misc]
280
+ attr = getattr(self.default, name)
281
+ if callable(attr):
282
+
283
+ @wraps(attr)
284
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
285
+ for key, arg in kwargs.items():
286
+ if key == "config" and (
287
+ isinstance(arg, dict)
288
+ and "configurable" in arg
289
+ and isinstance(arg["configurable"], dict)
290
+ ):
291
+ runnable, config = self.prepare(cast("RunnableConfig", arg))
292
+ kwargs = {**kwargs, "config": config}
293
+ return getattr(runnable, name)(*args, **kwargs)
294
+
295
+ for idx, arg in enumerate(args):
296
+ if (
297
+ isinstance(arg, dict)
298
+ and "configurable" in arg
299
+ and isinstance(arg["configurable"], dict)
300
+ ):
301
+ runnable, config = self.prepare(cast("RunnableConfig", arg))
302
+ argsl = list(args)
303
+ argsl[idx] = config
304
+ return getattr(runnable, name)(*argsl, **kwargs)
305
+
306
+ if self.config:
307
+ runnable, config = self.prepare()
308
+ return getattr(runnable, name)(*args, **kwargs)
309
+
310
+ return attr(*args, **kwargs)
311
+
312
+ return wrapper
313
+
314
+ return attr
315
+
316
+
317
+ class RunnableConfigurableFields(DynamicRunnable[Input, Output]):
318
+ """`Runnable` that can be dynamically configured.
319
+
320
+ A `RunnableConfigurableFields` should be initiated using the
321
+ `configurable_fields` method of a `Runnable`.
322
+
323
+ Here is an example of using a `RunnableConfigurableFields` with LLMs:
324
+
325
+ ```python
326
+ from coco_core.prompts import PromptTemplate
327
+ from coco_core.runnables import ConfigurableField
328
+ from coco_openai import ChatOpenAI
329
+
330
+ model = ChatOpenAI(temperature=0).configurable_fields(
331
+ temperature=ConfigurableField(
332
+ id="temperature",
333
+ name="LLM Temperature",
334
+ description="The temperature of the LLM",
335
+ )
336
+ )
337
+ # This creates a RunnableConfigurableFields for a chat model.
338
+
339
+ # When invoking the created RunnableSequence, you can pass in the
340
+ # value for your ConfigurableField's id which in this case
341
+ # will be change in temperature
342
+
343
+ prompt = PromptTemplate.from_template("Pick a random number above {x}")
344
+ chain = prompt | model
345
+
346
+ chain.invoke({"x": 0})
347
+ chain.invoke({"x": 0}, config={"configurable": {"temperature": 0.9}})
348
+ ```
349
+
350
+ Here is an example of using a `RunnableConfigurableFields` with `HubRunnables`:
351
+
352
+ ```python
353
+ from coco_core.prompts import PromptTemplate
354
+ from coco_core.runnables import ConfigurableField
355
+ from coco_openai import ChatOpenAI
356
+ from coco.runnables.hub import HubRunnable
357
+
358
+ prompt = HubRunnable("rlm/rag-prompt").configurable_fields(
359
+ owner_repo_commit=ConfigurableField(
360
+ id="hub_commit",
361
+ name="Hub Commit",
362
+ description="The Hub commit to pull from",
363
+ )
364
+ )
365
+
366
+ prompt.invoke({"question": "foo", "context": "bar"})
367
+
368
+ # Invoking prompt with `with_config` method
369
+
370
+ prompt.invoke(
371
+ {"question": "foo", "context": "bar"},
372
+ config={"configurable": {"hub_commit": "rlm/rag-prompt-llama"}},
373
+ )
374
+ ```
375
+ """
376
+
377
+ fields: dict[str, AnyConfigurableField]
378
+ """The configurable fields to use."""
379
+
380
+ @property
381
+ def config_specs(self) -> list[ConfigurableFieldSpec]:
382
+ """Get the configuration specs for the `RunnableConfigurableFields`.
383
+
384
+ Returns:
385
+ The configuration specs.
386
+ """
387
+ config_specs = []
388
+
389
+ default_fields = type(self.default).model_fields
390
+ for field_name, spec in self.fields.items():
391
+ if isinstance(spec, ConfigurableField):
392
+ config_specs.append(
393
+ ConfigurableFieldSpec(
394
+ id=spec.id,
395
+ name=spec.name,
396
+ description=spec.description
397
+ or default_fields[field_name].description,
398
+ annotation=spec.annotation
399
+ or default_fields[field_name].annotation,
400
+ default=getattr(self.default, field_name),
401
+ is_shared=spec.is_shared,
402
+ )
403
+ )
404
+ else:
405
+ config_specs.append(
406
+ make_options_spec(spec, default_fields[field_name].description)
407
+ )
408
+
409
+ config_specs.extend(self.default.config_specs)
410
+
411
+ return get_unique_config_specs(config_specs)
412
+
413
+ @override
414
+ def configurable_fields(
415
+ self, **kwargs: AnyConfigurableField
416
+ ) -> RunnableSerializable[Input, Output]:
417
+ return self.default.configurable_fields(**{**self.fields, **kwargs})
418
+
419
+ def _prepare(
420
+ self, config: RunnableConfig | None = None
421
+ ) -> tuple[Runnable[Input, Output], RunnableConfig]:
422
+ config = ensure_config(config)
423
+ specs_by_id = {spec.id: (key, spec) for key, spec in self.fields.items()}
424
+ configurable_fields = {
425
+ specs_by_id[k][0]: v
426
+ for k, v in config.get("configurable", {}).items()
427
+ if k in specs_by_id and isinstance(specs_by_id[k][1], ConfigurableField)
428
+ }
429
+ configurable_single_options = {
430
+ k: v.options[(config.get("configurable", {}).get(v.id) or v.default)]
431
+ for k, v in self.fields.items()
432
+ if isinstance(v, ConfigurableFieldSingleOption)
433
+ }
434
+ configurable_multi_options = {
435
+ k: [
436
+ v.options[o]
437
+ for o in config.get("configurable", {}).get(v.id, v.default)
438
+ ]
439
+ for k, v in self.fields.items()
440
+ if isinstance(v, ConfigurableFieldMultiOption)
441
+ }
442
+ configurable = {
443
+ **configurable_fields,
444
+ **configurable_single_options,
445
+ **configurable_multi_options,
446
+ }
447
+
448
+ if configurable:
449
+ init_params = {
450
+ k: v
451
+ for k, v in self.default.__dict__.items()
452
+ if k in type(self.default).model_fields
453
+ }
454
+ return (
455
+ self.default.__class__(**{**init_params, **configurable}),
456
+ config,
457
+ )
458
+ return (self.default, config)
459
+
460
+
461
+ # Before Python 3.11 native StrEnum is not available
462
+ class StrEnum(str, enum.Enum):
463
+ """String enum."""
464
+
465
+
466
+ _enums_for_spec: WeakValueDictionary[
467
+ ConfigurableFieldSingleOption | ConfigurableFieldMultiOption | ConfigurableField,
468
+ type[StrEnum],
469
+ ] = WeakValueDictionary()
470
+
471
+ _enums_for_spec_lock = threading.Lock()
472
+
473
+
474
+ class RunnableConfigurableAlternatives(DynamicRunnable[Input, Output]):
475
+ """`Runnable` that can be dynamically configured.
476
+
477
+ A `RunnableConfigurableAlternatives` should be initiated using the
478
+ `configurable_alternatives` method of a `Runnable` or can be
479
+ initiated directly as well.
480
+
481
+ Here is an example of using a `RunnableConfigurableAlternatives` that uses
482
+ alternative prompts to illustrate its functionality:
483
+
484
+ ```python
485
+ from coco_core.runnables import ConfigurableField
486
+ from coco_openai import ChatOpenAI
487
+
488
+ # This creates a RunnableConfigurableAlternatives for Prompt Runnable
489
+ # with two alternatives.
490
+ prompt = PromptTemplate.from_template(
491
+ "Tell me a joke about {topic}"
492
+ ).configurable_alternatives(
493
+ ConfigurableField(id="prompt"),
494
+ default_key="joke",
495
+ poem=PromptTemplate.from_template("Write a short poem about {topic}"),
496
+ )
497
+
498
+ # When invoking the created RunnableSequence, you can pass in the
499
+ # value for your ConfigurableField's id which in this case will either be
500
+ # `joke` or `poem`.
501
+ chain = prompt | ChatOpenAI(model="gpt-5.4-mini")
502
+
503
+ # The `with_config` method brings in the desired Prompt Runnable in your
504
+ # Runnable Sequence.
505
+ chain.with_config(configurable={"prompt": "poem"}).invoke({"topic": "bears"})
506
+ ```
507
+
508
+ Equivalently, you can initialize `RunnableConfigurableAlternatives` directly
509
+ and use in LCEL in the same way:
510
+
511
+ ```python
512
+ from coco_core.runnables import ConfigurableField
513
+ from coco_core.runnables.configurable import (
514
+ RunnableConfigurableAlternatives,
515
+ )
516
+ from coco_openai import ChatOpenAI
517
+
518
+ prompt = RunnableConfigurableAlternatives(
519
+ which=ConfigurableField(id="prompt"),
520
+ default=PromptTemplate.from_template("Tell me a joke about {topic}"),
521
+ default_key="joke",
522
+ prefix_keys=False,
523
+ alternatives={
524
+ "poem": PromptTemplate.from_template("Write a short poem about {topic}")
525
+ },
526
+ )
527
+ chain = prompt | ChatOpenAI(model="gpt-5.4-mini")
528
+ chain.with_config(configurable={"prompt": "poem"}).invoke({"topic": "bears"})
529
+ ```
530
+ """
531
+
532
+ which: ConfigurableField
533
+ """The `ConfigurableField` to use to choose between alternatives."""
534
+
535
+ alternatives: dict[
536
+ str,
537
+ Runnable[Input, Output] | Callable[[], Runnable[Input, Output]],
538
+ ]
539
+ """The alternatives to choose from."""
540
+
541
+ default_key: str = "default"
542
+ """The enum value to use for the default option."""
543
+
544
+ prefix_keys: bool
545
+ """Whether to prefix configurable fields of each alternative with a namespace
546
+ of the form <which.id>==<alternative_key>, e.g. a key named "temperature" used by
547
+ the alternative named "gpt3" becomes "model==gpt3/temperature".
548
+ """
549
+
550
+ @property
551
+ @override
552
+ def config_specs(self) -> list[ConfigurableFieldSpec]:
553
+ with _enums_for_spec_lock:
554
+ if which_enum := _enums_for_spec.get(self.which):
555
+ pass
556
+ else:
557
+ which_enum = StrEnum( # type: ignore[call-overload]
558
+ self.which.name or self.which.id,
559
+ (
560
+ (v, v)
561
+ for v in [*list(self.alternatives.keys()), self.default_key]
562
+ ),
563
+ )
564
+ _enums_for_spec[self.which] = cast("type[StrEnum]", which_enum)
565
+ return get_unique_config_specs(
566
+ # which alternative
567
+ [
568
+ ConfigurableFieldSpec(
569
+ id=self.which.id,
570
+ name=self.which.name,
571
+ description=self.which.description,
572
+ annotation=which_enum,
573
+ default=self.default_key,
574
+ is_shared=self.which.is_shared,
575
+ ),
576
+ ]
577
+ # config specs of the default option
578
+ + (
579
+ [
580
+ prefix_config_spec(s, f"{self.which.id}=={self.default_key}")
581
+ for s in self.default.config_specs
582
+ ]
583
+ if self.prefix_keys
584
+ else self.default.config_specs
585
+ )
586
+ # config specs of the alternatives
587
+ + [
588
+ (
589
+ prefix_config_spec(s, f"{self.which.id}=={alt_key}")
590
+ if self.prefix_keys
591
+ else s
592
+ )
593
+ for alt_key, alt in self.alternatives.items()
594
+ if isinstance(alt, RunnableSerializable)
595
+ for s in alt.config_specs
596
+ ]
597
+ )
598
+
599
+ @override
600
+ def configurable_fields(
601
+ self, **kwargs: AnyConfigurableField
602
+ ) -> RunnableSerializable[Input, Output]:
603
+ return self.__class__(
604
+ which=self.which,
605
+ default=self.default.configurable_fields(**kwargs),
606
+ alternatives=self.alternatives,
607
+ default_key=self.default_key,
608
+ prefix_keys=self.prefix_keys,
609
+ )
610
+
611
+ def _prepare(
612
+ self, config: RunnableConfig | None = None
613
+ ) -> tuple[Runnable[Input, Output], RunnableConfig]:
614
+ config = ensure_config(config)
615
+ which = config.get("configurable", {}).get(self.which.id, self.default_key)
616
+ # remap configurable keys for the chosen alternative
617
+ if self.prefix_keys:
618
+ config = cast(
619
+ "RunnableConfig",
620
+ {
621
+ **config,
622
+ "configurable": {
623
+ k.removeprefix(f"{self.which.id}=={which}/"): v
624
+ for k, v in config.get("configurable", {}).items()
625
+ },
626
+ },
627
+ )
628
+ # return the chosen alternative
629
+ if which == self.default_key:
630
+ return (self.default, config)
631
+ if which in self.alternatives:
632
+ alt = self.alternatives[which]
633
+ if isinstance(alt, Runnable):
634
+ return (alt, config)
635
+ return (alt(), config)
636
+ msg = f"Unknown alternative: {which}"
637
+ raise ValueError(msg)
638
+
639
+
640
+ def prefix_config_spec(
641
+ spec: ConfigurableFieldSpec, prefix: str
642
+ ) -> ConfigurableFieldSpec:
643
+ """Prefix the id of a `ConfigurableFieldSpec`.
644
+
645
+ This is useful when a `RunnableConfigurableAlternatives` is used as a
646
+ `ConfigurableField` of another `RunnableConfigurableAlternatives`.
647
+
648
+ Args:
649
+ spec: The `ConfigurableFieldSpec` to prefix.
650
+ prefix: The prefix to add.
651
+
652
+ Returns:
653
+ The prefixed `ConfigurableFieldSpec`.
654
+ """
655
+ return (
656
+ ConfigurableFieldSpec(
657
+ id=f"{prefix}/{spec.id}",
658
+ name=spec.name,
659
+ description=spec.description,
660
+ annotation=spec.annotation,
661
+ default=spec.default,
662
+ is_shared=spec.is_shared,
663
+ )
664
+ if not spec.is_shared
665
+ else spec
666
+ )
667
+
668
+
669
+ def make_options_spec(
670
+ spec: ConfigurableFieldSingleOption | ConfigurableFieldMultiOption,
671
+ description: str | None,
672
+ ) -> ConfigurableFieldSpec:
673
+ """Make options spec.
674
+
675
+ Make a `ConfigurableFieldSpec` for a `ConfigurableFieldSingleOption` or
676
+ `ConfigurableFieldMultiOption`.
677
+
678
+ Args:
679
+ spec: The `ConfigurableFieldSingleOption` or `ConfigurableFieldMultiOption`.
680
+ description: The description to use if the spec does not have one.
681
+
682
+ Returns:
683
+ The `ConfigurableFieldSpec`.
684
+ """
685
+ with _enums_for_spec_lock:
686
+ if enum := _enums_for_spec.get(spec):
687
+ pass
688
+ else:
689
+ enum = StrEnum( # type: ignore[call-overload]
690
+ spec.name or spec.id,
691
+ ((v, v) for v in list(spec.options.keys())),
692
+ )
693
+ _enums_for_spec[spec] = cast("type[StrEnum]", enum)
694
+ if isinstance(spec, ConfigurableFieldSingleOption):
695
+ return ConfigurableFieldSpec(
696
+ id=spec.id,
697
+ name=spec.name,
698
+ description=spec.description or description,
699
+ annotation=enum,
700
+ default=spec.default,
701
+ is_shared=spec.is_shared,
702
+ )
703
+ return ConfigurableFieldSpec(
704
+ id=spec.id,
705
+ name=spec.name,
706
+ description=spec.description or description,
707
+ annotation=Sequence[enum], # type: ignore[valid-type]
708
+ default=spec.default,
709
+ is_shared=spec.is_shared,
710
+ )