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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- 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 = " <br> ".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" -. {edge_data} .-> "
|
|
205
|
+
else:
|
|
206
|
+
edge_label = f" -- {edge_data} --> "
|
|
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)
|