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,741 @@
1
+ """Graph used in `Runnable` objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections import defaultdict
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ NamedTuple,
13
+ Protocol,
14
+ TypedDict,
15
+ overload,
16
+ )
17
+ from uuid import UUID, uuid4
18
+
19
+ from coco_core.load.serializable import to_json_not_implemented
20
+ from coco_core.runnables.base import Runnable, RunnableSerializable
21
+ from coco_core.utils.pydantic import (
22
+ TypeBaseModel,
23
+ _IgnoreUnserializable,
24
+ is_basemodel_subclass,
25
+ )
26
+
27
+ if TYPE_CHECKING:
28
+ from collections.abc import Callable, Sequence
29
+
30
+ from coco_core.runnables.base import Runnable as RunnableType
31
+
32
+
33
+ class Stringifiable(Protocol):
34
+ """Protocol for objects that can be converted to a string."""
35
+
36
+ def __str__(self) -> str:
37
+ """Convert the object to a string."""
38
+
39
+
40
+ class LabelsDict(TypedDict):
41
+ """Dictionary of labels for nodes and edges in a graph."""
42
+
43
+ nodes: dict[str, str]
44
+ """Labels for nodes."""
45
+ edges: dict[str, str]
46
+ """Labels for edges."""
47
+
48
+
49
+ def is_uuid(value: str) -> bool:
50
+ """Check if a string is a valid UUID.
51
+
52
+ Args:
53
+ value: The string to check.
54
+
55
+ Returns:
56
+ `True` if the string is a valid UUID, `False` otherwise.
57
+ """
58
+ try:
59
+ UUID(value)
60
+ except ValueError:
61
+ return False
62
+ return True
63
+
64
+
65
+ class Edge(NamedTuple):
66
+ """Edge in a graph."""
67
+
68
+ source: str
69
+ """The source node id."""
70
+ target: str
71
+ """The target node id."""
72
+ data: Stringifiable | None = None
73
+ """Optional data associated with the edge. """
74
+ conditional: bool = False
75
+ """Whether the edge is conditional."""
76
+
77
+ def copy(self, *, source: str | None = None, target: str | None = None) -> Edge:
78
+ """Return a copy of the edge with optional new source and target nodes.
79
+
80
+ Args:
81
+ source: The new source node id.
82
+ target: The new target node id.
83
+
84
+ Returns:
85
+ A copy of the edge with the new source and target nodes.
86
+ """
87
+ return Edge(
88
+ source=source or self.source,
89
+ target=target or self.target,
90
+ data=self.data,
91
+ conditional=self.conditional,
92
+ )
93
+
94
+
95
+ class Node(NamedTuple):
96
+ """Node in a graph."""
97
+
98
+ id: str
99
+ """The unique identifier of the node."""
100
+ name: str
101
+ """The name of the node."""
102
+ data: TypeBaseModel | RunnableType[Any, Any] | None
103
+ """The data of the node."""
104
+ metadata: dict[str, Any] | None
105
+ """Optional metadata for the node. """
106
+
107
+ def copy(
108
+ self,
109
+ *,
110
+ id: str | None = None,
111
+ name: str | None = None,
112
+ ) -> Node:
113
+ """Return a copy of the node with optional new id and name.
114
+
115
+ Args:
116
+ id: The new node id.
117
+ name: The new node name.
118
+
119
+ Returns:
120
+ A copy of the node with the new id and name.
121
+ """
122
+ return Node(
123
+ id=id or self.id,
124
+ name=name or self.name,
125
+ data=self.data,
126
+ metadata=self.metadata,
127
+ )
128
+
129
+
130
+ class Branch(NamedTuple):
131
+ """Branch in a graph."""
132
+
133
+ condition: Callable[..., str]
134
+ """A callable that returns a string representation of the condition."""
135
+ ends: dict[str, str] | None
136
+ """Optional dictionary of end node IDs for the branches. """
137
+
138
+
139
+ class CurveStyle(Enum):
140
+ """Enum for different curve styles supported by Mermaid."""
141
+
142
+ BASIS = "basis"
143
+ BUMP_X = "bumpX"
144
+ BUMP_Y = "bumpY"
145
+ CARDINAL = "cardinal"
146
+ CATMULL_ROM = "catmullRom"
147
+ LINEAR = "linear"
148
+ MONOTONE_X = "monotoneX"
149
+ MONOTONE_Y = "monotoneY"
150
+ NATURAL = "natural"
151
+ STEP = "step"
152
+ STEP_AFTER = "stepAfter"
153
+ STEP_BEFORE = "stepBefore"
154
+
155
+
156
+ @dataclass
157
+ class NodeStyles:
158
+ """Schema for Hexadecimal color codes for different node types.
159
+
160
+ Args:
161
+ default: The default color code.
162
+ first: The color code for the first node.
163
+ last: The color code for the last node.
164
+ """
165
+
166
+ default: str = "fill:#f2f0ff,line-height:1.2"
167
+ first: str = "fill-opacity:0"
168
+ last: str = "fill:#bfb6fc"
169
+
170
+
171
+ class MermaidDrawMethod(Enum):
172
+ """Enum for different draw methods supported by Mermaid."""
173
+
174
+ PYPPETEER = "pyppeteer"
175
+ """Uses Pyppeteer to render the graph"""
176
+ API = "api"
177
+ """Uses Mermaid.INK API to render the graph"""
178
+
179
+
180
+ def node_data_str(
181
+ id: str,
182
+ data: TypeBaseModel | RunnableType[Any, Any] | None,
183
+ ) -> str:
184
+ """Convert the data of a node to a string.
185
+
186
+ Args:
187
+ id: The node id.
188
+ data: The node data.
189
+
190
+ Returns:
191
+ A string representation of the data.
192
+ """
193
+ if not is_uuid(id) or data is None:
194
+ return id
195
+ data_str = data.get_name() if isinstance(data, Runnable) else data.__name__
196
+ return data_str if not data_str.startswith("Runnable") else data_str[8:]
197
+
198
+
199
+ def node_data_json(
200
+ node: Node, *, with_schemas: bool = False
201
+ ) -> dict[str, str | dict[str, Any]]:
202
+ """Convert the data of a node to a JSON-serializable format.
203
+
204
+ Args:
205
+ node: The `Node` to convert.
206
+ with_schemas: Whether to include the schema of the data if it is a Pydantic
207
+ model.
208
+
209
+ Returns:
210
+ A dictionary with the type of the data and the data itself.
211
+ """
212
+ if node.data is None:
213
+ json: dict[str, Any] = {}
214
+ elif isinstance(node.data, RunnableSerializable):
215
+ json = {
216
+ "type": "runnable",
217
+ "data": {
218
+ "id": node.data.lc_id(),
219
+ "name": node_data_str(node.id, node.data),
220
+ },
221
+ }
222
+ elif isinstance(node.data, Runnable):
223
+ json = {
224
+ "type": "runnable",
225
+ "data": {
226
+ "id": to_json_not_implemented(node.data)["id"],
227
+ "name": node_data_str(node.id, node.data),
228
+ },
229
+ }
230
+ elif inspect.isclass(node.data) and is_basemodel_subclass(node.data):
231
+ json = (
232
+ {
233
+ "type": "schema",
234
+ "data": node.data.model_json_schema(
235
+ schema_generator=_IgnoreUnserializable
236
+ ),
237
+ }
238
+ if with_schemas
239
+ else {
240
+ "type": "schema",
241
+ "data": node_data_str(node.id, node.data),
242
+ }
243
+ )
244
+ else:
245
+ json = {
246
+ "type": "unknown",
247
+ "data": node_data_str(node.id, node.data),
248
+ }
249
+ if node.metadata is not None:
250
+ json["metadata"] = node.metadata
251
+ return json
252
+
253
+
254
+ @dataclass
255
+ class Graph:
256
+ """Graph of nodes and edges.
257
+
258
+ Args:
259
+ nodes: Dictionary of nodes in the graph. Defaults to an empty dictionary.
260
+ edges: List of edges in the graph. Defaults to an empty list.
261
+ """
262
+
263
+ nodes: dict[str, Node] = field(default_factory=dict)
264
+ edges: list[Edge] = field(default_factory=list)
265
+
266
+ def to_json(self, *, with_schemas: bool = False) -> dict[str, list[dict[str, Any]]]:
267
+ """Convert the graph to a JSON-serializable format.
268
+
269
+ Args:
270
+ with_schemas: Whether to include the schemas of the nodes if they are
271
+ Pydantic models.
272
+
273
+ Returns:
274
+ A dictionary with the nodes and edges of the graph.
275
+ """
276
+ stable_node_ids = {
277
+ node.id: i if is_uuid(node.id) else node.id
278
+ for i, node in enumerate(self.nodes.values())
279
+ }
280
+ edges: list[dict[str, Any]] = []
281
+ for edge in self.edges:
282
+ edge_dict = {
283
+ "source": stable_node_ids[edge.source],
284
+ "target": stable_node_ids[edge.target],
285
+ }
286
+ if edge.data is not None:
287
+ edge_dict["data"] = edge.data # type: ignore[assignment]
288
+ if edge.conditional:
289
+ edge_dict["conditional"] = True
290
+ edges.append(edge_dict)
291
+
292
+ return {
293
+ "nodes": [
294
+ {
295
+ "id": stable_node_ids[node.id],
296
+ **node_data_json(node, with_schemas=with_schemas),
297
+ }
298
+ for node in self.nodes.values()
299
+ ],
300
+ "edges": edges,
301
+ }
302
+
303
+ def __bool__(self) -> bool:
304
+ """Return whether the graph has any nodes."""
305
+ return bool(self.nodes)
306
+
307
+ def next_id(self) -> str:
308
+ """Return a new unique node identifier.
309
+
310
+ It that can be used to add a node to the graph.
311
+ """
312
+ return uuid4().hex
313
+
314
+ def add_node(
315
+ self,
316
+ data: TypeBaseModel | RunnableType[Any, Any] | None,
317
+ id: str | None = None,
318
+ *,
319
+ metadata: dict[str, Any] | None = None,
320
+ ) -> Node:
321
+ """Add a node to the graph and return it.
322
+
323
+ Args:
324
+ data: The data of the node.
325
+ id: The id of the node.
326
+ metadata: Optional metadata for the node.
327
+
328
+ Returns:
329
+ The node that was added to the graph.
330
+
331
+ Raises:
332
+ ValueError: If a node with the same id already exists.
333
+ """
334
+ if id is not None and id in self.nodes:
335
+ msg = f"Node with id {id} already exists"
336
+ raise ValueError(msg)
337
+ id_ = id or self.next_id()
338
+ node = Node(id=id_, data=data, metadata=metadata, name=node_data_str(id_, data))
339
+ self.nodes[node.id] = node
340
+ return node
341
+
342
+ def remove_node(self, node: Node) -> None:
343
+ """Remove a node from the graph and all edges connected to it.
344
+
345
+ Args:
346
+ node: The node to remove.
347
+ """
348
+ self.nodes.pop(node.id)
349
+ self.edges = [
350
+ edge for edge in self.edges if node.id not in {edge.source, edge.target}
351
+ ]
352
+
353
+ def add_edge(
354
+ self,
355
+ source: Node,
356
+ target: Node,
357
+ data: Stringifiable | None = None,
358
+ conditional: bool = False, # noqa: FBT001,FBT002
359
+ ) -> Edge:
360
+ """Add an edge to the graph and return it.
361
+
362
+ Args:
363
+ source: The source node of the edge.
364
+ target: The target node of the edge.
365
+ data: Optional data associated with the edge.
366
+ conditional: Whether the edge is conditional.
367
+
368
+ Returns:
369
+ The edge that was added to the graph.
370
+
371
+ Raises:
372
+ ValueError: If the source or target node is not in the graph.
373
+ """
374
+ if source.id not in self.nodes:
375
+ msg = f"Source node {source.id} not in graph"
376
+ raise ValueError(msg)
377
+ if target.id not in self.nodes:
378
+ msg = f"Target node {target.id} not in graph"
379
+ raise ValueError(msg)
380
+ edge = Edge(
381
+ source=source.id, target=target.id, data=data, conditional=conditional
382
+ )
383
+ self.edges.append(edge)
384
+ return edge
385
+
386
+ def extend(
387
+ self, graph: Graph, *, prefix: str = ""
388
+ ) -> tuple[Node | None, Node | None]:
389
+ """Add all nodes and edges from another graph.
390
+
391
+ Note this doesn't check for duplicates, nor does it connect the graphs.
392
+
393
+ Args:
394
+ graph: The graph to add.
395
+ prefix: The prefix to add to the node ids.
396
+
397
+ Returns:
398
+ A tuple of the first and last nodes of the subgraph.
399
+ """
400
+ if all(is_uuid(node.id) for node in graph.nodes.values()):
401
+ prefix = ""
402
+
403
+ def prefixed(id_: str) -> str:
404
+ return f"{prefix}:{id_}" if prefix else id_
405
+
406
+ # prefix each node
407
+ self.nodes.update(
408
+ {prefixed(k): v.copy(id=prefixed(k)) for k, v in graph.nodes.items()}
409
+ )
410
+ # prefix each edge's source and target
411
+ self.edges.extend(
412
+ [
413
+ edge.copy(source=prefixed(edge.source), target=prefixed(edge.target))
414
+ for edge in graph.edges
415
+ ]
416
+ )
417
+ # return (prefixed) first and last nodes of the subgraph
418
+ first, last = graph.first_node(), graph.last_node()
419
+ return (
420
+ first.copy(id=prefixed(first.id)) if first else None,
421
+ last.copy(id=prefixed(last.id)) if last else None,
422
+ )
423
+
424
+ def reid(self) -> Graph:
425
+ """Return a new graph with all nodes re-identified.
426
+
427
+ Uses their unique, readable names where possible.
428
+ """
429
+ node_name_to_ids = defaultdict(list)
430
+ for node in self.nodes.values():
431
+ node_name_to_ids[node.name].append(node.id)
432
+
433
+ unique_labels = {
434
+ node_id: node_name if len(node_ids) == 1 else f"{node_name}_{i + 1}"
435
+ for node_name, node_ids in node_name_to_ids.items()
436
+ for i, node_id in enumerate(node_ids)
437
+ }
438
+
439
+ def _get_node_id(node_id: str) -> str:
440
+ label = unique_labels[node_id]
441
+ if is_uuid(node_id):
442
+ return label
443
+ return node_id
444
+
445
+ return Graph(
446
+ nodes={
447
+ _get_node_id(id_): node.copy(id=_get_node_id(id_))
448
+ for id_, node in self.nodes.items()
449
+ },
450
+ edges=[
451
+ edge.copy(
452
+ source=_get_node_id(edge.source),
453
+ target=_get_node_id(edge.target),
454
+ )
455
+ for edge in self.edges
456
+ ],
457
+ )
458
+
459
+ def first_node(self) -> Node | None:
460
+ """Find the single node that is not a target of any edge.
461
+
462
+ If there is no such node, or there are multiple, return `None`.
463
+ When drawing the graph, this node would be the origin.
464
+
465
+ Returns:
466
+ The first node, or None if there is no such node or multiple
467
+ candidates.
468
+ """
469
+ return _first_node(self)
470
+
471
+ def last_node(self) -> Node | None:
472
+ """Find the single node that is not a source of any edge.
473
+
474
+ If there is no such node, or there are multiple, return `None`.
475
+ When drawing the graph, this node would be the destination.
476
+
477
+ Returns:
478
+ The last node, or None if there is no such node or multiple
479
+ candidates.
480
+ """
481
+ return _last_node(self)
482
+
483
+ def trim_first_node(self) -> None:
484
+ """Remove the first node if it exists and has a single outgoing edge.
485
+
486
+ i.e., if removing it would not leave the graph without a "first" node.
487
+ """
488
+ first_node = self.first_node()
489
+ if (
490
+ first_node
491
+ and _first_node(self, exclude=[first_node.id])
492
+ and len({e for e in self.edges if e.source == first_node.id}) == 1
493
+ ):
494
+ self.remove_node(first_node)
495
+
496
+ def trim_last_node(self) -> None:
497
+ """Remove the last node if it exists and has a single incoming edge.
498
+
499
+ i.e., if removing it would not leave the graph without a "last" node.
500
+ """
501
+ last_node = self.last_node()
502
+ if (
503
+ last_node
504
+ and _last_node(self, exclude=[last_node.id])
505
+ and len({e for e in self.edges if e.target == last_node.id}) == 1
506
+ ):
507
+ self.remove_node(last_node)
508
+
509
+ def draw_ascii(self) -> str:
510
+ """Draw the graph as an ASCII art string.
511
+
512
+ Returns:
513
+ The ASCII art string.
514
+ """
515
+ # Import locally to prevent circular import
516
+ from coco_core.runnables.graph_ascii import draw_ascii # noqa: PLC0415
517
+
518
+ return draw_ascii(
519
+ {node.id: node.name for node in self.nodes.values()},
520
+ self.edges,
521
+ )
522
+
523
+ def print_ascii(self) -> None:
524
+ """Print the graph as an ASCII art string."""
525
+ print(self.draw_ascii()) # noqa: T201
526
+
527
+ @overload
528
+ def draw_png(
529
+ self,
530
+ output_file_path: str,
531
+ fontname: str | None = None,
532
+ labels: LabelsDict | None = None,
533
+ ) -> None: ...
534
+
535
+ @overload
536
+ def draw_png(
537
+ self,
538
+ output_file_path: None,
539
+ fontname: str | None = None,
540
+ labels: LabelsDict | None = None,
541
+ ) -> bytes: ...
542
+
543
+ def draw_png(
544
+ self,
545
+ output_file_path: str | None = None,
546
+ fontname: str | None = None,
547
+ labels: LabelsDict | None = None,
548
+ ) -> bytes | None:
549
+ """Draw the graph as a PNG image.
550
+
551
+ Args:
552
+ output_file_path: The path to save the image to. If `None`, the image
553
+ is not saved.
554
+ fontname: The name of the font to use.
555
+ labels: Optional labels for nodes and edges in the graph. Defaults to
556
+ `None`.
557
+
558
+ Returns:
559
+ The PNG image as bytes if output_file_path is None, None otherwise.
560
+ """
561
+ # Import locally to prevent circular import
562
+ from coco_core.runnables.graph_png import PngDrawer # noqa: PLC0415
563
+
564
+ default_node_labels = {node.id: node.name for node in self.nodes.values()}
565
+
566
+ return PngDrawer(
567
+ fontname,
568
+ LabelsDict(
569
+ nodes={
570
+ **default_node_labels,
571
+ **(labels["nodes"] if labels is not None else {}),
572
+ },
573
+ edges=labels["edges"] if labels is not None else {},
574
+ ),
575
+ ).draw(self, output_file_path)
576
+
577
+ def draw_mermaid(
578
+ self,
579
+ *,
580
+ with_styles: bool = True,
581
+ curve_style: CurveStyle = CurveStyle.LINEAR,
582
+ node_colors: NodeStyles | None = None,
583
+ wrap_label_n_words: int = 9,
584
+ frontmatter_config: dict[str, Any] | None = None,
585
+ ) -> str:
586
+ """Draw the graph as a Mermaid syntax string.
587
+
588
+ Args:
589
+ with_styles: Whether to include styles in the syntax.
590
+ curve_style: The style of the edges.
591
+ node_colors: The colors of the nodes.
592
+ wrap_label_n_words: The number of words to wrap the node labels at.
593
+ frontmatter_config: Mermaid frontmatter config.
594
+ Can be used to customize theme and styles. Will be converted to YAML and
595
+ added to the beginning of the mermaid graph.
596
+
597
+ See more here: https://mermaid.js.org/config/configuration.html.
598
+
599
+ Example config:
600
+
601
+ ```python
602
+ {
603
+ "config": {
604
+ "theme": "neutral",
605
+ "look": "handDrawn",
606
+ "themeVariables": {"primaryColor": "#e2e2e2"},
607
+ }
608
+ }
609
+ ```
610
+ Returns:
611
+ The Mermaid syntax string.
612
+ """
613
+ # Import locally to prevent circular import
614
+ from coco_core.runnables.graph_mermaid import draw_mermaid # noqa: PLC0415
615
+
616
+ graph = self.reid()
617
+ first_node = graph.first_node()
618
+ last_node = graph.last_node()
619
+
620
+ return draw_mermaid(
621
+ nodes=graph.nodes,
622
+ edges=graph.edges,
623
+ first_node=first_node.id if first_node else None,
624
+ last_node=last_node.id if last_node else None,
625
+ with_styles=with_styles,
626
+ curve_style=curve_style,
627
+ node_styles=node_colors,
628
+ wrap_label_n_words=wrap_label_n_words,
629
+ frontmatter_config=frontmatter_config,
630
+ )
631
+
632
+ def draw_mermaid_png(
633
+ self,
634
+ *,
635
+ curve_style: CurveStyle = CurveStyle.LINEAR,
636
+ node_colors: NodeStyles | None = None,
637
+ wrap_label_n_words: int = 9,
638
+ output_file_path: str | None = None,
639
+ draw_method: MermaidDrawMethod = MermaidDrawMethod.API,
640
+ background_color: str = "white",
641
+ padding: int = 10,
642
+ max_retries: int = 1,
643
+ retry_delay: float = 1.0,
644
+ frontmatter_config: dict[str, Any] | None = None,
645
+ base_url: str | None = None,
646
+ proxies: dict[str, str] | None = None,
647
+ ) -> bytes:
648
+ """Draw the graph as a PNG image using Mermaid.
649
+
650
+ Args:
651
+ curve_style: The style of the edges.
652
+ node_colors: The colors of the nodes.
653
+ wrap_label_n_words: The number of words to wrap the node labels at.
654
+ output_file_path: The path to save the image to. If `None`, the image
655
+ is not saved.
656
+ draw_method: The method to use to draw the graph.
657
+ background_color: The color of the background.
658
+ padding: The padding around the graph.
659
+ max_retries: The maximum number of retries (`MermaidDrawMethod.API`).
660
+ retry_delay: The delay between retries (`MermaidDrawMethod.API`).
661
+ frontmatter_config: Mermaid frontmatter config.
662
+ Can be used to customize theme and styles. Will be converted to YAML and
663
+ added to the beginning of the mermaid graph.
664
+
665
+ See more here: https://mermaid.js.org/config/configuration.html.
666
+
667
+ Example config:
668
+
669
+ ```python
670
+ {
671
+ "config": {
672
+ "theme": "neutral",
673
+ "look": "handDrawn",
674
+ "themeVariables": {"primaryColor": "#e2e2e2"},
675
+ }
676
+ }
677
+ ```
678
+ base_url: The base URL of the Mermaid server for rendering via API.
679
+ proxies: HTTP/HTTPS proxies for requests (e.g. `{"http": "http://127.0.0.1:7890"}`).
680
+
681
+ Returns:
682
+ The PNG image as bytes.
683
+ """
684
+ # Import locally to prevent circular import
685
+ from coco_core.runnables.graph_mermaid import ( # noqa: PLC0415
686
+ draw_mermaid_png,
687
+ )
688
+
689
+ mermaid_syntax = self.draw_mermaid(
690
+ curve_style=curve_style,
691
+ node_colors=node_colors,
692
+ wrap_label_n_words=wrap_label_n_words,
693
+ frontmatter_config=frontmatter_config,
694
+ )
695
+ return draw_mermaid_png(
696
+ mermaid_syntax=mermaid_syntax,
697
+ output_file_path=output_file_path,
698
+ draw_method=draw_method,
699
+ background_color=background_color,
700
+ padding=padding,
701
+ max_retries=max_retries,
702
+ retry_delay=retry_delay,
703
+ proxies=proxies,
704
+ base_url=base_url,
705
+ )
706
+
707
+
708
+ def _first_node(graph: Graph, exclude: Sequence[str] = ()) -> Node | None:
709
+ """Find the single node that is not a target of any edge.
710
+
711
+ Exclude nodes/sources with IDs in the exclude list.
712
+
713
+ If there is no such node, or there are multiple, return `None`.
714
+
715
+ When drawing the graph, this node would be the origin.
716
+ """
717
+ targets = {edge.target for edge in graph.edges if edge.source not in exclude}
718
+ found: list[Node] = [
719
+ node
720
+ for node in graph.nodes.values()
721
+ if node.id not in exclude and node.id not in targets
722
+ ]
723
+ return found[0] if len(found) == 1 else None
724
+
725
+
726
+ def _last_node(graph: Graph, exclude: Sequence[str] = ()) -> Node | None:
727
+ """Find the single node that is not a source of any edge.
728
+
729
+ Exclude nodes/targets with IDs in the exclude list.
730
+
731
+ If there is no such node, or there are multiple, return `None`.
732
+
733
+ When drawing the graph, this node would be the destination.
734
+ """
735
+ sources = {edge.source for edge in graph.edges if edge.target not in exclude}
736
+ found: list[Node] = [
737
+ node
738
+ for node in graph.nodes.values()
739
+ if node.id not in exclude and node.id not in sources
740
+ ]
741
+ return found[0] if len(found) == 1 else None