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,503 @@
1
+ """Mermaid graph drawing utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import base64
7
+ import random
8
+ import re
9
+ import string
10
+ import time
11
+ import urllib.parse
12
+ from dataclasses import asdict
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Any, Literal, cast
15
+
16
+ import yaml
17
+
18
+ from coco_core.runnables.graph import (
19
+ CurveStyle,
20
+ MermaidDrawMethod,
21
+ NodeStyles,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ from coco_core.runnables.graph import Edge, Node
26
+
27
+
28
+ try:
29
+ import requests
30
+
31
+ _HAS_REQUESTS = True
32
+ except ImportError:
33
+ _HAS_REQUESTS = False
34
+
35
+ try:
36
+ from pyppeteer import launch # type: ignore[import-not-found]
37
+
38
+ _HAS_PYPPETEER = True
39
+ except ImportError:
40
+ _HAS_PYPPETEER = False
41
+
42
+ MARKDOWN_SPECIAL_CHARS = "*_`"
43
+
44
+
45
+ def draw_mermaid(
46
+ nodes: dict[str, Node],
47
+ edges: list[Edge],
48
+ *,
49
+ first_node: str | None = None,
50
+ last_node: str | None = None,
51
+ with_styles: bool = True,
52
+ curve_style: CurveStyle = CurveStyle.LINEAR,
53
+ node_styles: NodeStyles | None = None,
54
+ wrap_label_n_words: int = 9,
55
+ frontmatter_config: dict[str, Any] | None = None,
56
+ ) -> str:
57
+ """Draws a Mermaid graph using the provided graph data.
58
+
59
+ Args:
60
+ nodes: List of node ids.
61
+ edges: List of edges, object with a source, target and data.
62
+ first_node: Id of the first node.
63
+ last_node: Id of the last node.
64
+ with_styles: Whether to include styles in the graph.
65
+ curve_style: Curve style for the edges.
66
+ node_styles: Node colors for different types.
67
+ wrap_label_n_words: Words to wrap the edge labels.
68
+ frontmatter_config: Mermaid frontmatter config.
69
+ Can be used to customize theme and styles. Will be converted to YAML and
70
+ added to the beginning of the mermaid graph.
71
+
72
+ See more here: https://mermaid.js.org/config/configuration.html.
73
+
74
+ Example config:
75
+
76
+ ```python
77
+ {
78
+ "config": {
79
+ "theme": "neutral",
80
+ "look": "handDrawn",
81
+ "themeVariables": {"primaryColor": "#e2e2e2"},
82
+ }
83
+ }
84
+ ```
85
+
86
+ Returns:
87
+ Mermaid graph syntax.
88
+
89
+ """
90
+ # Initialize Mermaid graph configuration
91
+ original_frontmatter_config = frontmatter_config or {}
92
+ original_flowchart_config = original_frontmatter_config.get("config", {}).get(
93
+ "flowchart", {}
94
+ )
95
+ frontmatter_config = {
96
+ **original_frontmatter_config,
97
+ "config": {
98
+ **original_frontmatter_config.get("config", {}),
99
+ "flowchart": {**original_flowchart_config, "curve": curve_style.value},
100
+ },
101
+ }
102
+
103
+ mermaid_graph = (
104
+ (
105
+ "---\n"
106
+ + yaml.dump(frontmatter_config, default_flow_style=False)
107
+ + "---\ngraph TD;\n"
108
+ )
109
+ if with_styles
110
+ else "graph TD;\n"
111
+ )
112
+ # Group nodes by subgraph
113
+ subgraph_nodes: dict[str, dict[str, Node]] = {}
114
+ regular_nodes: dict[str, Node] = {}
115
+
116
+ for key, node in nodes.items():
117
+ if ":" in key:
118
+ # For nodes with colons, add them only to their deepest subgraph level
119
+ prefix = ":".join(key.split(":")[:-1])
120
+ subgraph_nodes.setdefault(prefix, {})[key] = node
121
+ else:
122
+ regular_nodes[key] = node
123
+
124
+ # Node formatting templates
125
+ default_class_label = "default"
126
+ format_dict = {default_class_label: "{0}({1})"}
127
+ if first_node is not None:
128
+ format_dict[first_node] = "{0}([{1}]):::first"
129
+ if last_node is not None:
130
+ format_dict[last_node] = "{0}([{1}]):::last"
131
+
132
+ def render_node(key: str, node: Node, indent: str = "\t") -> str:
133
+ """Helper function to render a node with consistent formatting."""
134
+ node_name = node.name.split(":")[-1]
135
+ label = (
136
+ f"<p>{node_name}</p>"
137
+ if node_name.startswith(tuple(MARKDOWN_SPECIAL_CHARS))
138
+ and node_name.endswith(tuple(MARKDOWN_SPECIAL_CHARS))
139
+ else node_name
140
+ )
141
+ if node.metadata:
142
+ label = (
143
+ f"{label}<hr/><small><em>"
144
+ + "\n".join(f"{k} = {value}" for k, value in node.metadata.items())
145
+ + "</em></small>"
146
+ )
147
+ node_label = format_dict.get(key, format_dict[default_class_label]).format(
148
+ _to_safe_id(key), label
149
+ )
150
+ return f"{indent}{node_label}\n"
151
+
152
+ # Add non-subgraph nodes to the graph
153
+ if with_styles:
154
+ for key, node in regular_nodes.items():
155
+ mermaid_graph += render_node(key, node)
156
+
157
+ # Group edges by their common prefixes
158
+ edge_groups: dict[str, list[Edge]] = {}
159
+ for edge in edges:
160
+ src_parts = edge.source.split(":")
161
+ tgt_parts = edge.target.split(":")
162
+ common_prefix = ":".join(
163
+ src for src, tgt in zip(src_parts, tgt_parts, strict=False) if src == tgt
164
+ )
165
+ edge_groups.setdefault(common_prefix, []).append(edge)
166
+
167
+ seen_subgraphs = set()
168
+
169
+ def add_subgraph(edges: list[Edge], prefix: str) -> None:
170
+ nonlocal mermaid_graph
171
+ self_loop = len(edges) == 1 and edges[0].source == edges[0].target
172
+ if prefix and not self_loop:
173
+ subgraph = prefix.rsplit(":", maxsplit=1)[-1]
174
+ if subgraph in seen_subgraphs:
175
+ msg = (
176
+ f"Found duplicate subgraph '{subgraph}' -- this likely means that "
177
+ "you're reusing a subgraph node with the same name. "
178
+ "Please adjust your graph to have subgraph nodes with unique names."
179
+ )
180
+ raise ValueError(msg)
181
+
182
+ seen_subgraphs.add(subgraph)
183
+ mermaid_graph += f"\tsubgraph {subgraph}\n"
184
+
185
+ # Add nodes that belong to this subgraph
186
+ if with_styles and prefix in subgraph_nodes:
187
+ for key, node in subgraph_nodes[prefix].items():
188
+ mermaid_graph += render_node(key, node)
189
+
190
+ for edge in edges:
191
+ source, target = edge.source, edge.target
192
+
193
+ # Add BR every wrap_label_n_words words
194
+ if edge.data is not None:
195
+ edge_data = edge.data
196
+ words = str(edge_data).split() # Split the string into words
197
+ # Group words into chunks of wrap_label_n_words size
198
+ if len(words) > wrap_label_n_words:
199
+ edge_data = "&nbsp<br>&nbsp".join(
200
+ " ".join(words[i : i + wrap_label_n_words])
201
+ for i in range(0, len(words), wrap_label_n_words)
202
+ )
203
+ if edge.conditional:
204
+ edge_label = f" -. &nbsp;{edge_data}&nbsp; .-> "
205
+ else:
206
+ edge_label = f" -- &nbsp;{edge_data}&nbsp; --> "
207
+ else:
208
+ edge_label = " -.-> " if edge.conditional else " --> "
209
+
210
+ mermaid_graph += (
211
+ f"\t{_to_safe_id(source)}{edge_label}{_to_safe_id(target)};\n"
212
+ )
213
+
214
+ # Recursively add nested subgraphs
215
+ for nested_prefix, edges_ in edge_groups.items():
216
+ if not nested_prefix.startswith(prefix + ":") or nested_prefix == prefix:
217
+ continue
218
+ # only go to first level subgraphs
219
+ if ":" in nested_prefix[len(prefix) + 1 :]:
220
+ continue
221
+ add_subgraph(edges_, nested_prefix)
222
+
223
+ if prefix and not self_loop:
224
+ mermaid_graph += "\tend\n"
225
+
226
+ # Start with the top-level edges (no common prefix)
227
+ add_subgraph(edge_groups.get("", []), "")
228
+
229
+ # Add remaining subgraphs with edges
230
+ for prefix, edges_ in edge_groups.items():
231
+ if not prefix or ":" in prefix:
232
+ continue
233
+ add_subgraph(edges_, prefix)
234
+ seen_subgraphs.add(prefix)
235
+
236
+ # Add empty subgraphs (subgraphs with no internal edges)
237
+ if with_styles:
238
+ for prefix, subgraph_node in subgraph_nodes.items():
239
+ if ":" not in prefix and prefix not in seen_subgraphs:
240
+ mermaid_graph += f"\tsubgraph {prefix}\n"
241
+
242
+ # Add nodes that belong to this subgraph
243
+ for key, node in subgraph_node.items():
244
+ mermaid_graph += render_node(key, node)
245
+
246
+ mermaid_graph += "\tend\n"
247
+ seen_subgraphs.add(prefix)
248
+
249
+ # Add custom styles for nodes
250
+ if with_styles:
251
+ mermaid_graph += _generate_mermaid_graph_styles(node_styles or NodeStyles())
252
+ return mermaid_graph
253
+
254
+
255
+ def _to_safe_id(label: str) -> str:
256
+ """Convert a string into a Mermaid-compatible node id.
257
+
258
+ Keep [a-zA-Z0-9_-] characters unchanged.
259
+ Map every other character -> backslash + lowercase hex codepoint.
260
+
261
+ Result is guaranteed to be unique and Mermaid-compatible,
262
+ so nodes with special characters always render correctly.
263
+ """
264
+ allowed = string.ascii_letters + string.digits + "_-"
265
+ out = [ch if ch in allowed else "\\" + format(ord(ch), "x") for ch in label]
266
+ return "".join(out)
267
+
268
+
269
+ def _generate_mermaid_graph_styles(node_colors: NodeStyles) -> str:
270
+ """Generates Mermaid graph styles for different node types."""
271
+ styles = ""
272
+ for class_name, style in asdict(node_colors).items():
273
+ styles += f"\tclassDef {class_name} {style}\n"
274
+ return styles
275
+
276
+
277
+ def draw_mermaid_png(
278
+ mermaid_syntax: str,
279
+ output_file_path: str | None = None,
280
+ draw_method: MermaidDrawMethod = MermaidDrawMethod.API,
281
+ background_color: str | None = "white",
282
+ padding: int = 10,
283
+ max_retries: int = 1,
284
+ retry_delay: float = 1.0,
285
+ base_url: str | None = None,
286
+ proxies: dict[str, str] | None = None,
287
+ ) -> bytes:
288
+ """Draws a Mermaid graph as PNG using provided syntax.
289
+
290
+ Args:
291
+ mermaid_syntax: Mermaid graph syntax.
292
+ output_file_path: Path to save the PNG image.
293
+ draw_method: Method to draw the graph.
294
+ background_color: Background color of the image.
295
+ padding: Padding around the image.
296
+ max_retries: Maximum number of retries (MermaidDrawMethod.API).
297
+ retry_delay: Delay between retries (MermaidDrawMethod.API).
298
+ base_url: Base URL for the Mermaid.ink API.
299
+ proxies: HTTP/HTTPS proxies for requests (e.g. `{"http": "http://127.0.0.1:7890"}`).
300
+
301
+ Returns:
302
+ PNG image bytes.
303
+
304
+ Raises:
305
+ ValueError: If an invalid draw method is provided.
306
+ """
307
+ if draw_method == MermaidDrawMethod.PYPPETEER:
308
+ img_bytes = asyncio.run(
309
+ _render_mermaid_using_pyppeteer(
310
+ mermaid_syntax, output_file_path, background_color, padding
311
+ )
312
+ )
313
+ elif draw_method == MermaidDrawMethod.API:
314
+ img_bytes = _render_mermaid_using_api(
315
+ mermaid_syntax,
316
+ output_file_path=output_file_path,
317
+ background_color=background_color,
318
+ max_retries=max_retries,
319
+ retry_delay=retry_delay,
320
+ base_url=base_url,
321
+ proxies=proxies,
322
+ )
323
+ else:
324
+ supported_methods = ", ".join([m.value for m in MermaidDrawMethod]) # type: ignore[unreachable]
325
+ msg = (
326
+ f"Invalid draw method: {draw_method}. "
327
+ f"Supported draw methods are: {supported_methods}"
328
+ )
329
+ raise ValueError(msg)
330
+
331
+ return img_bytes
332
+
333
+
334
+ async def _render_mermaid_using_pyppeteer(
335
+ mermaid_syntax: str,
336
+ output_file_path: str | None = None,
337
+ background_color: str | None = "white",
338
+ padding: int = 10,
339
+ device_scale_factor: int = 3,
340
+ ) -> bytes:
341
+ """Renders Mermaid graph using Pyppeteer."""
342
+ if not _HAS_PYPPETEER:
343
+ msg = "Install Pyppeteer to use the Pyppeteer method: `pip install pyppeteer`."
344
+ raise ImportError(msg)
345
+
346
+ browser = await launch()
347
+ page = await browser.newPage()
348
+
349
+ # Setup Mermaid JS
350
+ await page.goto("about:blank")
351
+ await page.addScriptTag(
352
+ {"url": "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"}
353
+ )
354
+ await page.evaluate(
355
+ """() => {
356
+ mermaid.initialize({startOnLoad:true});
357
+ }"""
358
+ )
359
+
360
+ # Render SVG
361
+ svg_code = await page.evaluate(
362
+ """(mermaidGraph) => {
363
+ return mermaid.mermaidAPI.render('mermaid', mermaidGraph);
364
+ }""",
365
+ mermaid_syntax,
366
+ )
367
+
368
+ # Set the page background to white
369
+ await page.evaluate(
370
+ """(svg, background_color) => {
371
+ document.body.innerHTML = svg;
372
+ document.body.style.background = background_color;
373
+ }""",
374
+ svg_code["svg"],
375
+ background_color,
376
+ )
377
+
378
+ # Take a screenshot
379
+ dimensions = await page.evaluate(
380
+ """() => {
381
+ const svgElement = document.querySelector('svg');
382
+ const rect = svgElement.getBoundingClientRect();
383
+ return { width: rect.width, height: rect.height };
384
+ }"""
385
+ )
386
+ await page.setViewport(
387
+ {
388
+ "width": int(dimensions["width"] + padding),
389
+ "height": int(dimensions["height"] + padding),
390
+ "deviceScaleFactor": device_scale_factor,
391
+ }
392
+ )
393
+
394
+ img_bytes = cast("bytes", await page.screenshot({"fullPage": False}))
395
+ await browser.close()
396
+
397
+ if output_file_path is not None:
398
+ await asyncio.get_running_loop().run_in_executor(
399
+ None, Path(output_file_path).write_bytes, img_bytes
400
+ )
401
+
402
+ return img_bytes
403
+
404
+
405
+ def _render_mermaid_using_api(
406
+ mermaid_syntax: str,
407
+ *,
408
+ output_file_path: str | None = None,
409
+ background_color: str | None = "white",
410
+ file_type: Literal["jpeg", "png", "webp"] | None = "png",
411
+ max_retries: int = 1,
412
+ retry_delay: float = 1.0,
413
+ proxies: dict[str, str] | None = None,
414
+ base_url: str | None = None,
415
+ ) -> bytes:
416
+ """Renders Mermaid graph using the Mermaid.INK API."""
417
+ # Automated scanners: please do not open security advisories about `base_url`
418
+ # here. This parameter is only ever explicitly controlled by developers, and it
419
+ # is not exposed by default over the network by the framework, so this is not
420
+ # an SSRF sink unless an application explicitly wires untrusted input into it.
421
+ # It exists to support user-managed Mermaid API deployments.
422
+ # Defaults to using the public mermaid.ink server.
423
+ base_url = base_url if base_url is not None else "https://mermaid.ink"
424
+
425
+ if not _HAS_REQUESTS:
426
+ msg = (
427
+ "Install the `requests` module to use the Mermaid.INK API: "
428
+ "`pip install requests`."
429
+ )
430
+ raise ImportError(msg)
431
+
432
+ # Use Mermaid API to render the image
433
+ mermaid_syntax_encoded = base64.b64encode(mermaid_syntax.encode("utf8")).decode(
434
+ "ascii"
435
+ )
436
+
437
+ # Check if the background color is a hexadecimal color code using regex
438
+ if background_color is not None:
439
+ hex_color_pattern = re.compile(r"^#(?:[0-9a-fA-F]{3}){1,2}$")
440
+ if not hex_color_pattern.match(background_color):
441
+ background_color = f"!{background_color}"
442
+
443
+ # URL-encode the background_color to handle special characters like '!'
444
+ encoded_bg_color = urllib.parse.quote(str(background_color), safe="")
445
+ image_url = (
446
+ f"{base_url}/img/{mermaid_syntax_encoded}"
447
+ f"?type={file_type}&bgColor={encoded_bg_color}"
448
+ )
449
+
450
+ error_msg_suffix = (
451
+ "To resolve this issue:\n"
452
+ "1. Check your internet connection and try again\n"
453
+ "2. Try with higher retry settings: "
454
+ "`draw_mermaid_png(..., max_retries=5, retry_delay=2.0)`\n"
455
+ "3. Use the Pyppeteer rendering method which will render your graph locally "
456
+ "in a browser: `draw_mermaid_png(..., draw_method=MermaidDrawMethod.PYPPETEER)`"
457
+ )
458
+
459
+ for attempt in range(max_retries + 1):
460
+ try:
461
+ response = requests.get(image_url, timeout=10, proxies=proxies)
462
+ if response.status_code == requests.codes.ok:
463
+ img_bytes = response.content
464
+ if output_file_path is not None:
465
+ Path(output_file_path).write_bytes(response.content)
466
+
467
+ return img_bytes
468
+
469
+ # If we get a server error (5xx), retry
470
+ if (
471
+ requests.codes.internal_server_error <= response.status_code
472
+ and attempt < max_retries
473
+ ):
474
+ # Exponential backoff with jitter
475
+ sleep_time = retry_delay * (2**attempt) * (0.5 + 0.5 * random.random()) # noqa: S311 not used for crypto
476
+ time.sleep(sleep_time)
477
+ continue
478
+
479
+ # For other status codes, fail immediately
480
+ msg = (
481
+ f"Failed to reach {base_url} API while trying to render "
482
+ f"your graph. Status code: {response.status_code}.\n\n"
483
+ ) + error_msg_suffix
484
+ raise ValueError(msg)
485
+
486
+ except (requests.RequestException, requests.Timeout) as e:
487
+ if attempt < max_retries:
488
+ # Exponential backoff with jitter
489
+ sleep_time = retry_delay * (2**attempt) * (0.5 + 0.5 * random.random()) # noqa: S311 not used for crypto
490
+ time.sleep(sleep_time)
491
+ else:
492
+ msg = (
493
+ f"Failed to reach {base_url} API while trying to render "
494
+ f"your graph after {max_retries} retries. "
495
+ ) + error_msg_suffix
496
+ raise ValueError(msg) from e
497
+
498
+ # This should not be reached, but just in case
499
+ msg = (
500
+ f"Failed to reach {base_url} API while trying to render "
501
+ f"your graph after {max_retries} retries. "
502
+ ) + error_msg_suffix
503
+ raise ValueError(msg)