fast-agent-mcp 0.2.45__py3-none-any.whl → 0.2.47__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.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (35) hide show
  1. {fast_agent_mcp-0.2.45.dist-info → fast_agent_mcp-0.2.47.dist-info}/METADATA +13 -13
  2. {fast_agent_mcp-0.2.45.dist-info → fast_agent_mcp-0.2.47.dist-info}/RECORD +35 -30
  3. mcp_agent/__init__.py +40 -0
  4. mcp_agent/agents/workflow/iterative_planner.py +572 -0
  5. mcp_agent/agents/workflow/orchestrator_agent.py +3 -3
  6. mcp_agent/agents/workflow/orchestrator_models.py +6 -6
  7. mcp_agent/cli/commands/go.py +25 -4
  8. mcp_agent/core/__init__.py +26 -0
  9. mcp_agent/core/agent_types.py +1 -0
  10. mcp_agent/core/direct_decorators.py +168 -16
  11. mcp_agent/core/direct_factory.py +42 -15
  12. mcp_agent/core/fastagent.py +4 -0
  13. mcp_agent/core/mermaid_utils.py +170 -0
  14. mcp_agent/human_input/__init__.py +50 -0
  15. mcp_agent/human_input/form_fields.py +252 -0
  16. mcp_agent/human_input/simple_form.py +111 -0
  17. mcp_agent/llm/augmented_llm.py +11 -2
  18. mcp_agent/llm/augmented_llm_playback.py +5 -3
  19. mcp_agent/llm/model_database.py +2 -7
  20. mcp_agent/llm/providers/augmented_llm_aliyun.py +1 -1
  21. mcp_agent/llm/providers/augmented_llm_anthropic.py +1 -1
  22. mcp_agent/llm/providers/augmented_llm_deepseek.py +4 -2
  23. mcp_agent/llm/providers/augmented_llm_google_oai.py +1 -1
  24. mcp_agent/llm/providers/augmented_llm_openrouter.py +1 -1
  25. mcp_agent/llm/providers/augmented_llm_tensorzero.py +1 -1
  26. mcp_agent/llm/providers/augmented_llm_xai.py +1 -1
  27. mcp_agent/mcp/__init__.py +50 -0
  28. mcp_agent/mcp/helpers/__init__.py +23 -1
  29. mcp_agent/mcp/interfaces.py +13 -2
  30. mcp_agent/py.typed +0 -0
  31. mcp_agent/resources/examples/workflows/orchestrator.py +5 -2
  32. mcp_agent/ui/console_display.py +104 -39
  33. {fast_agent_mcp-0.2.45.dist-info → fast_agent_mcp-0.2.47.dist-info}/WHEEL +0 -0
  34. {fast_agent_mcp-0.2.45.dist-info → fast_agent_mcp-0.2.47.dist-info}/entry_points.txt +0 -0
  35. {fast_agent_mcp-0.2.45.dist-info → fast_agent_mcp-0.2.47.dist-info}/licenses/LICENSE +0 -0
@@ -22,6 +22,7 @@ class AgentType(Enum):
22
22
  EVALUATOR_OPTIMIZER = "evaluator_optimizer"
23
23
  ROUTER = "router"
24
24
  CHAIN = "chain"
25
+ ITERATIVE_PLANNER = "iterative_planner"
25
26
 
26
27
 
27
28
  @dataclass
@@ -6,6 +6,7 @@ for creating agents in the DirectFastAgent framework.
6
6
 
7
7
  import inspect
8
8
  from functools import wraps
9
+ from pathlib import Path
9
10
  from typing import (
10
11
  Awaitable,
11
12
  Callable,
@@ -21,8 +22,10 @@ from typing import (
21
22
  )
22
23
 
23
24
  from mcp.client.session import ElicitationFnT
25
+ from pydantic import AnyUrl
24
26
 
25
27
  from mcp_agent.agents.agent import AgentConfig
28
+ from mcp_agent.agents.workflow.iterative_planner import ITERATIVE_PLAN_SYSTEM_PROMPT_TEMPLATE
26
29
  from mcp_agent.agents.workflow.router_agent import (
27
30
  ROUTING_SYSTEM_INSTRUCTION,
28
31
  )
@@ -85,6 +88,91 @@ class DecoratedEvaluatorOptimizerProtocol(DecoratedAgentProtocol[P, R], Protocol
85
88
  _evaluator: str
86
89
 
87
90
 
91
+ def _fetch_url_content(url: str) -> str:
92
+ """
93
+ Fetch content from a URL.
94
+
95
+ Args:
96
+ url: The URL to fetch content from
97
+
98
+ Returns:
99
+ The text content from the URL
100
+
101
+ Raises:
102
+ requests.RequestException: If the URL cannot be fetched
103
+ UnicodeDecodeError: If the content cannot be decoded as UTF-8
104
+ """
105
+ import requests
106
+
107
+ response = requests.get(url, timeout=10)
108
+ response.raise_for_status() # Raise exception for HTTP errors
109
+ return response.text
110
+
111
+
112
+ def _apply_templates(text: str) -> str:
113
+ """
114
+ Apply template substitutions to instruction text.
115
+
116
+ Supported templates:
117
+ {{currentDate}} - Current date in format "24 July 2025"
118
+ {{url:https://...}} - Content fetched from the specified URL
119
+
120
+ Args:
121
+ text: The text to process
122
+
123
+ Returns:
124
+ Text with template substitutions applied
125
+
126
+ Raises:
127
+ requests.RequestException: If a URL in {{url:...}} cannot be fetched
128
+ UnicodeDecodeError: If URL content cannot be decoded as UTF-8
129
+ """
130
+ import re
131
+ from datetime import datetime
132
+
133
+ # Apply {{currentDate}} template
134
+ current_date = datetime.now().strftime("%d %B %Y")
135
+ text = text.replace("{{currentDate}}", current_date)
136
+
137
+ # Apply {{url:...}} templates
138
+ url_pattern = re.compile(r"\{\{url:(https?://[^}]+)\}\}")
139
+
140
+ def replace_url(match):
141
+ url = match.group(1)
142
+ return _fetch_url_content(url)
143
+
144
+ text = url_pattern.sub(replace_url, text)
145
+
146
+ return text
147
+
148
+
149
+ def _resolve_instruction(instruction: str | Path | AnyUrl) -> str:
150
+ """
151
+ Resolve instruction from either a string, Path, or URL with template support.
152
+
153
+ Args:
154
+ instruction: Either a string instruction, Path to a file, or URL containing the instruction
155
+
156
+ Returns:
157
+ The resolved instruction string with templates applied
158
+
159
+ Raises:
160
+ FileNotFoundError: If the Path doesn't exist
161
+ PermissionError: If the Path can't be read
162
+ UnicodeDecodeError: If the file/URL content can't be decoded as UTF-8
163
+ requests.RequestException: If the URL cannot be fetched
164
+ """
165
+ if isinstance(instruction, Path):
166
+ text = instruction.read_text(encoding="utf-8")
167
+ elif isinstance(instruction, AnyUrl):
168
+ text = _fetch_url_content(str(instruction))
169
+ else:
170
+ text = instruction
171
+
172
+ # Apply template substitutions
173
+ return _apply_templates(text)
174
+
175
+
88
176
  def _decorator_impl(
89
177
  self,
90
178
  agent_type: AgentType,
@@ -183,9 +271,9 @@ def _decorator_impl(
183
271
  def agent(
184
272
  self,
185
273
  name: str = "default",
186
- instruction_or_kwarg: Optional[str] = None,
274
+ instruction_or_kwarg: Optional[str | Path | AnyUrl] = None,
187
275
  *,
188
- instruction: str = "You are a helpful agent.",
276
+ instruction: str | Path | AnyUrl = "You are a helpful agent.",
189
277
  servers: List[str] = [],
190
278
  tools: Optional[Dict[str, List[str]]] = None,
191
279
  resources: Optional[Dict[str, List[str]]] = None,
@@ -220,7 +308,10 @@ def agent(
220
308
  Returns:
221
309
  A decorator that registers the agent with proper type annotations
222
310
  """
223
- final_instruction = instruction_or_kwarg if instruction_or_kwarg is not None else instruction
311
+ final_instruction_raw = (
312
+ instruction_or_kwarg if instruction_or_kwarg is not None else instruction
313
+ )
314
+ final_instruction = _resolve_instruction(final_instruction_raw)
224
315
 
225
316
  return _decorator_impl(
226
317
  self,
@@ -245,9 +336,9 @@ def custom(
245
336
  self,
246
337
  cls,
247
338
  name: str = "default",
248
- instruction_or_kwarg: Optional[str] = None,
339
+ instruction_or_kwarg: Optional[str | Path | AnyUrl] = None,
249
340
  *,
250
- instruction: str = "You are a helpful agent.",
341
+ instruction: str | Path | AnyUrl = "You are a helpful agent.",
251
342
  servers: List[str] = [],
252
343
  tools: Optional[Dict[str, List[str]]] = None,
253
344
  resources: Optional[Dict[str, List[str]]] = None,
@@ -277,7 +368,10 @@ def custom(
277
368
  Returns:
278
369
  A decorator that registers the agent with proper type annotations
279
370
  """
280
- final_instruction = instruction_or_kwarg if instruction_or_kwarg is not None else instruction
371
+ final_instruction_raw = (
372
+ instruction_or_kwarg if instruction_or_kwarg is not None else instruction
373
+ )
374
+ final_instruction = _resolve_instruction(final_instruction_raw)
281
375
 
282
376
  return _decorator_impl(
283
377
  self,
@@ -311,7 +405,7 @@ def orchestrator(
311
405
  name: str,
312
406
  *,
313
407
  agents: List[str],
314
- instruction: str = DEFAULT_INSTRUCTION_ORCHESTRATOR,
408
+ instruction: str | Path | AnyUrl = DEFAULT_INSTRUCTION_ORCHESTRATOR,
315
409
  model: Optional[str] = None,
316
410
  request_params: RequestParams | None = None,
317
411
  use_history: bool = False,
@@ -341,6 +435,7 @@ def orchestrator(
341
435
  """
342
436
 
343
437
  # Create final request params with plan_iterations
438
+ resolved_instruction = _resolve_instruction(instruction)
344
439
 
345
440
  return cast(
346
441
  "Callable[[AgentCallable[P, R]], DecoratedOrchestratorProtocol[P, R]]",
@@ -348,7 +443,7 @@ def orchestrator(
348
443
  self,
349
444
  AgentType.ORCHESTRATOR,
350
445
  name=name,
351
- instruction=instruction,
446
+ instruction=resolved_instruction,
352
447
  servers=[], # Orchestrators don't connect to servers directly
353
448
  model=model,
354
449
  use_history=use_history,
@@ -363,12 +458,65 @@ def orchestrator(
363
458
  )
364
459
 
365
460
 
461
+ def iterative_planner(
462
+ self,
463
+ name: str,
464
+ *,
465
+ agents: List[str],
466
+ instruction: str | Path | AnyUrl = ITERATIVE_PLAN_SYSTEM_PROMPT_TEMPLATE,
467
+ model: Optional[str] = None,
468
+ request_params: RequestParams | None = None,
469
+ plan_iterations: int = -1,
470
+ default: bool = False,
471
+ api_key: str | None = None,
472
+ ) -> Callable[[AgentCallable[P, R]], DecoratedOrchestratorProtocol[P, R]]:
473
+ """
474
+ Decorator to create and register an orchestrator agent with type-safe signature.
475
+
476
+ Args:
477
+ name: Name of the orchestrator
478
+ agents: List of agent names this orchestrator can use
479
+ instruction: Base instruction for the orchestrator
480
+ model: Model specification string
481
+ use_history: Whether to maintain conversation history
482
+ request_params: Additional request parameters for the LLM
483
+ human_input: Whether to enable human input capabilities
484
+ plan_type: Planning approach - "full" or "iterative"
485
+ plan_iterations: Maximum number of planning iterations (0 for unlimited)
486
+ default: Whether to mark this as the default agent
487
+
488
+ Returns:
489
+ A decorator that registers the orchestrator with proper type annotations
490
+ """
491
+
492
+ # Create final request params with plan_iterations
493
+ resolved_instruction = _resolve_instruction(instruction)
494
+
495
+ return cast(
496
+ "Callable[[AgentCallable[P, R]], DecoratedOrchestratorProtocol[P, R]]",
497
+ _decorator_impl(
498
+ self,
499
+ AgentType.ITERATIVE_PLANNER,
500
+ name=name,
501
+ instruction=resolved_instruction,
502
+ servers=[], # Orchestrators don't connect to servers directly
503
+ model=model,
504
+ use_history=False,
505
+ request_params=request_params,
506
+ child_agents=agents,
507
+ plan_iterations=plan_iterations,
508
+ default=default,
509
+ api_key=api_key,
510
+ ),
511
+ )
512
+
513
+
366
514
  def router(
367
515
  self,
368
516
  name: str,
369
517
  *,
370
518
  agents: List[str],
371
- instruction: Optional[str] = None,
519
+ instruction: Optional[str | Path | AnyUrl] = None,
372
520
  servers: List[str] = [],
373
521
  tools: Optional[Dict[str, List[str]]] = None,
374
522
  resources: Optional[Dict[str, List[str]]] = None,
@@ -400,6 +548,7 @@ def router(
400
548
  Returns:
401
549
  A decorator that registers the router with proper type annotations
402
550
  """
551
+ resolved_instruction = _resolve_instruction(instruction or ROUTING_SYSTEM_INSTRUCTION)
403
552
 
404
553
  return cast(
405
554
  "Callable[[AgentCallable[P, R]], DecoratedRouterProtocol[P, R]]",
@@ -407,7 +556,7 @@ def router(
407
556
  self,
408
557
  AgentType.ROUTER,
409
558
  name=name,
410
- instruction=instruction or ROUTING_SYSTEM_INSTRUCTION,
559
+ instruction=resolved_instruction,
411
560
  servers=servers,
412
561
  model=model,
413
562
  use_history=use_history,
@@ -429,7 +578,7 @@ def chain(
429
578
  name: str,
430
579
  *,
431
580
  sequence: List[str],
432
- instruction: Optional[str] = None,
581
+ instruction: Optional[str | Path | AnyUrl] = None,
433
582
  cumulative: bool = False,
434
583
  default: bool = False,
435
584
  ) -> Callable[[AgentCallable[P, R]], DecoratedChainProtocol[P, R]]:
@@ -456,6 +605,7 @@ def chain(
456
605
  You are a chain that processes requests through a series of specialized agents in sequence.
457
606
  Pass the output of each agent to the next agent in the chain.
458
607
  """
608
+ resolved_instruction = _resolve_instruction(instruction or default_instruction)
459
609
 
460
610
  return cast(
461
611
  "Callable[[AgentCallable[P, R]], DecoratedChainProtocol[P, R]]",
@@ -463,7 +613,7 @@ def chain(
463
613
  self,
464
614
  AgentType.CHAIN,
465
615
  name=name,
466
- instruction=instruction or default_instruction,
616
+ instruction=resolved_instruction,
467
617
  sequence=sequence,
468
618
  cumulative=cumulative,
469
619
  default=default,
@@ -477,7 +627,7 @@ def parallel(
477
627
  *,
478
628
  fan_out: List[str],
479
629
  fan_in: str | None = None,
480
- instruction: Optional[str] = None,
630
+ instruction: Optional[str | Path | AnyUrl] = None,
481
631
  include_request: bool = True,
482
632
  default: bool = False,
483
633
  ) -> Callable[[AgentCallable[P, R]], DecoratedParallelProtocol[P, R]]:
@@ -499,6 +649,7 @@ def parallel(
499
649
  You are a parallel processor that executes multiple agents simultaneously
500
650
  and aggregates their results.
501
651
  """
652
+ resolved_instruction = _resolve_instruction(instruction or default_instruction)
502
653
 
503
654
  return cast(
504
655
  "Callable[[AgentCallable[P, R]], DecoratedParallelProtocol[P, R]]",
@@ -506,7 +657,7 @@ def parallel(
506
657
  self,
507
658
  AgentType.PARALLEL,
508
659
  name=name,
509
- instruction=instruction or default_instruction,
660
+ instruction=resolved_instruction,
510
661
  servers=[], # Parallel agents don't connect to servers directly
511
662
  fan_in=fan_in,
512
663
  fan_out=fan_out,
@@ -522,7 +673,7 @@ def evaluator_optimizer(
522
673
  *,
523
674
  generator: str,
524
675
  evaluator: str,
525
- instruction: Optional[str] = None,
676
+ instruction: Optional[str | Path | AnyUrl] = None,
526
677
  min_rating: str = "GOOD",
527
678
  max_refinements: int = 3,
528
679
  default: bool = False,
@@ -547,6 +698,7 @@ def evaluator_optimizer(
547
698
  evaluated for quality, and then refined based on specific feedback until
548
699
  it reaches an acceptable quality standard.
549
700
  """
701
+ resolved_instruction = _resolve_instruction(instruction or default_instruction)
550
702
 
551
703
  return cast(
552
704
  "Callable[[AgentCallable[P, R]], DecoratedEvaluatorOptimizerProtocol[P, R]]",
@@ -554,7 +706,7 @@ def evaluator_optimizer(
554
706
  self,
555
707
  AgentType.EVALUATOR_OPTIMIZER,
556
708
  name=name,
557
- instruction=instruction or default_instruction,
709
+ instruction=resolved_instruction,
558
710
  servers=[], # Evaluator-optimizer doesn't connect to servers directly
559
711
  generator=generator,
560
712
  evaluator=evaluator,
@@ -10,6 +10,7 @@ from mcp_agent.agents.workflow.evaluator_optimizer import (
10
10
  EvaluatorOptimizerAgent,
11
11
  QualityRating,
12
12
  )
13
+ from mcp_agent.agents.workflow.iterative_planner import IterativePlanner
13
14
  from mcp_agent.agents.workflow.orchestrator_agent import OrchestratorAgent
14
15
  from mcp_agent.agents.workflow.parallel_agent import ParallelAgent
15
16
  from mcp_agent.agents.workflow.router_agent import RouterAgent
@@ -21,9 +22,10 @@ from mcp_agent.event_progress import ProgressAction
21
22
  from mcp_agent.llm.augmented_llm import RequestParams
22
23
  from mcp_agent.llm.model_factory import ModelFactory
23
24
  from mcp_agent.logging.logger import get_logger
25
+ from mcp_agent.mcp.interfaces import AgentProtocol
24
26
 
25
27
  # Type aliases for improved readability and IDE support
26
- AgentDict = Dict[str, Agent]
28
+ AgentDict = Dict[str, AgentProtocol]
27
29
  AgentConfigDict = Dict[str, Dict[str, Any]]
28
30
  T = TypeVar("T") # For generic types
29
31
 
@@ -153,7 +155,7 @@ async def create_agents_by_type(
153
155
  await agent.attach_llm(
154
156
  llm_factory,
155
157
  request_params=config.default_request_params,
156
- api_key=config.api_key
158
+ api_key=config.api_key,
157
159
  )
158
160
  result_agents[name] = agent
159
161
 
@@ -172,11 +174,11 @@ async def create_agents_by_type(
172
174
  await agent.attach_llm(
173
175
  llm_factory,
174
176
  request_params=config.default_request_params,
175
- api_key=config.api_key
177
+ api_key=config.api_key,
176
178
  )
177
179
  result_agents[name] = agent
178
180
 
179
- elif agent_type == AgentType.ORCHESTRATOR:
181
+ elif agent_type == AgentType.ORCHESTRATOR or agent_type == AgentType.ITERATIVE_PLANNER:
180
182
  # Get base params configured with model settings
181
183
  base_params = (
182
184
  config.default_request_params.model_copy()
@@ -193,24 +195,35 @@ async def create_agents_by_type(
193
195
  agent = active_agents[agent_name]
194
196
  child_agents.append(agent)
195
197
 
196
- # Create the orchestrator
197
- orchestrator = OrchestratorAgent(
198
- config=config,
199
- context=app_instance.context,
200
- agents=child_agents,
201
- plan_iterations=agent_data.get("plan_iterations", 5),
202
- plan_type=agent_data.get("plan_type", "full"),
203
- )
198
+ if AgentType.ORCHESTRATOR == agent_type:
199
+ # Create the orchestrator
200
+ orchestrator = OrchestratorAgent(
201
+ config=config,
202
+ context=app_instance.context,
203
+ agents=child_agents,
204
+ plan_iterations=agent_data.get("plan_iterations", 5),
205
+ plan_type=agent_data.get("plan_type", "full"),
206
+ )
207
+ else:
208
+ orchestrator = IterativePlanner(
209
+ config=config,
210
+ context=app_instance.context,
211
+ agents=child_agents,
212
+ plan_iterations=agent_data.get("plan_iterations", 5),
213
+ plan_type=agent_data.get("plan_type", "full"),
214
+ )
204
215
 
205
216
  # Initialize the orchestrator
206
217
  await orchestrator.initialize()
207
218
 
208
219
  # Attach LLM to the orchestrator
209
220
  llm_factory = model_factory_func(model=config.model)
221
+
222
+ # print("************", config.default_request_params.instruction)
210
223
  await orchestrator.attach_llm(
211
224
  llm_factory,
212
225
  request_params=config.default_request_params,
213
- api_key=config.api_key
226
+ api_key=config.api_key,
214
227
  )
215
228
 
216
229
  result_agents[name] = orchestrator
@@ -274,7 +287,7 @@ async def create_agents_by_type(
274
287
  await router.attach_llm(
275
288
  llm_factory,
276
289
  request_params=config.default_request_params,
277
- api_key=config.api_key
290
+ api_key=config.api_key,
278
291
  )
279
292
  result_agents[name] = router
280
293
 
@@ -461,7 +474,6 @@ async def create_agents_in_dependency_order(
461
474
  )
462
475
  active_agents.update(evaluator_agents)
463
476
 
464
- # Create orchestrator agents last since they might depend on other agents
465
477
  if AgentType.ORCHESTRATOR.value in [agents_dict[name]["type"] for name in group]:
466
478
  orchestrator_agents = await create_agents_by_type(
467
479
  app_instance,
@@ -476,6 +488,21 @@ async def create_agents_in_dependency_order(
476
488
  )
477
489
  active_agents.update(orchestrator_agents)
478
490
 
491
+ # Create orchestrator2 agents last since they might depend on other agents
492
+ if AgentType.ITERATIVE_PLANNER.value in [agents_dict[name]["type"] for name in group]:
493
+ orchestrator2_agents = await create_agents_by_type(
494
+ app_instance,
495
+ {
496
+ name: agents_dict[name]
497
+ for name in group
498
+ if agents_dict[name]["type"] == AgentType.ITERATIVE_PLANNER.value
499
+ },
500
+ AgentType.ITERATIVE_PLANNER,
501
+ active_agents,
502
+ model_factory_func,
503
+ )
504
+ active_agents.update(orchestrator2_agents)
505
+
479
506
  return active_agents
480
507
 
481
508
 
@@ -31,6 +31,9 @@ from mcp_agent.core.direct_decorators import (
31
31
  from mcp_agent.core.direct_decorators import (
32
32
  evaluator_optimizer as evaluator_optimizer_decorator,
33
33
  )
34
+ from mcp_agent.core.direct_decorators import (
35
+ iterative_planner as orchestrator2_decorator,
36
+ )
34
37
  from mcp_agent.core.direct_decorators import (
35
38
  orchestrator as orchestrator_decorator,
36
39
  )
@@ -249,6 +252,7 @@ class FastAgent:
249
252
  agent = agent_decorator
250
253
  custom = custom_decorator
251
254
  orchestrator = orchestrator_decorator
255
+ iterative_planner = orchestrator2_decorator
252
256
  router = router_decorator
253
257
  chain = chain_decorator
254
258
  parallel = parallel_decorator
@@ -0,0 +1,170 @@
1
+ """Utilities for detecting and processing Mermaid diagrams in text content."""
2
+
3
+ import base64
4
+ import re
5
+ import zlib
6
+ from dataclasses import dataclass
7
+ from typing import List, Optional
8
+
9
+ # Mermaid chart viewer URL prefix
10
+ MERMAID_VIEWER_URL = "https://www.mermaidchart.com/play#"
11
+ # mermaid.live#pako= also works but the playground has better ux
12
+
13
+
14
+ @dataclass
15
+ class MermaidDiagram:
16
+ """Represents a detected Mermaid diagram."""
17
+
18
+ content: str
19
+ title: Optional[str] = None
20
+ start_pos: int = 0
21
+ end_pos: int = 0
22
+
23
+
24
+ def extract_mermaid_diagrams(text: str) -> List[MermaidDiagram]:
25
+ """
26
+ Extract all Mermaid diagram blocks from text content.
27
+
28
+ Handles both simple mermaid blocks and blocks with titles:
29
+ - ```mermaid
30
+ - ```mermaid title={Some Title}
31
+
32
+ Also extracts titles from within the diagram content.
33
+
34
+ Args:
35
+ text: The text content to search for Mermaid diagrams
36
+
37
+ Returns:
38
+ List of MermaidDiagram objects found in the text
39
+ """
40
+ diagrams = []
41
+
42
+ # Pattern to match mermaid code blocks with optional title
43
+ # Matches: ```mermaid or ```mermaid title={...}
44
+ pattern = r"```mermaid(?:\s+title=\{([^}]+)\})?\s*\n(.*?)```"
45
+
46
+ for match in re.finditer(pattern, text, re.DOTALL):
47
+ title = match.group(1) # May be None if no title
48
+ content = match.group(2).strip()
49
+
50
+ if content: # Only add if there's actual diagram content
51
+ # If no title from code fence, look for title in the content
52
+ if not title:
53
+ # Look for various title patterns in mermaid diagrams
54
+ # pie title, graph title, etc.
55
+ title_patterns = [
56
+ r"^\s*title\s+(.+?)(?:\n|$)", # Generic title
57
+ r"^\s*pie\s+title\s+(.+?)(?:\n|$)", # Pie chart title
58
+ r"^\s*gantt\s+title\s+(.+?)(?:\n|$)", # Gantt chart title
59
+ ]
60
+
61
+ for title_pattern in title_patterns:
62
+ title_match = re.search(title_pattern, content, re.MULTILINE)
63
+ if title_match:
64
+ title = title_match.group(1).strip()
65
+ break
66
+
67
+ diagrams.append(
68
+ MermaidDiagram(
69
+ content=content, title=title, start_pos=match.start(), end_pos=match.end()
70
+ )
71
+ )
72
+
73
+ return diagrams
74
+
75
+
76
+ def create_mermaid_live_link(diagram_content: str) -> str:
77
+ """
78
+ Create a Mermaid Live Editor link from diagram content.
79
+
80
+ The link uses pako compression (zlib) and base64 encoding.
81
+
82
+ Args:
83
+ diagram_content: The Mermaid diagram source code
84
+
85
+ Returns:
86
+ Complete URL to Mermaid Live Editor
87
+ """
88
+ # Create the JSON structure expected by Mermaid Live
89
+ # Escape newlines and quotes in the diagram content
90
+ escaped_content = diagram_content.replace('"', '\\"').replace("\n", "\\n")
91
+ json_str = f'{{"code":"{escaped_content}","mermaid":{{"theme":"default"}},"updateEditor":false,"autoSync":true,"updateDiagram":false}}'
92
+
93
+ # Compress using zlib (pako compatible)
94
+ compressed = zlib.compress(json_str.encode("utf-8"))
95
+
96
+ # Base64 encode
97
+ encoded = base64.urlsafe_b64encode(compressed).decode("utf-8")
98
+
99
+ # Remove padding characters as Mermaid Live doesn't use them
100
+ encoded = encoded.rstrip("=")
101
+
102
+ return f"{MERMAID_VIEWER_URL}pako:{encoded}"
103
+
104
+
105
+ def format_mermaid_links(diagrams: List[MermaidDiagram]) -> List[str]:
106
+ """
107
+ Format Mermaid diagrams as markdown links.
108
+
109
+ Args:
110
+ diagrams: List of MermaidDiagram objects
111
+
112
+ Returns:
113
+ List of formatted markdown strings
114
+ """
115
+ links = []
116
+
117
+ for i, diagram in enumerate(diagrams, 1):
118
+ link = create_mermaid_live_link(diagram.content)
119
+
120
+ if diagram.title:
121
+ # Use the title from the diagram with number
122
+ markdown = f"Diagram {i} - {diagram.title}: [Open Diagram]({link})"
123
+ else:
124
+ # Use generic numbering
125
+ markdown = f"Diagram {i}: [Open Diagram]({link})"
126
+
127
+ links.append(markdown)
128
+
129
+ return links
130
+
131
+
132
+ def detect_diagram_type(content: str) -> str:
133
+ """
134
+ Detect the type of mermaid diagram from content.
135
+
136
+ Args:
137
+ content: The mermaid diagram source code
138
+
139
+ Returns:
140
+ Human-readable diagram type name
141
+ """
142
+ content_lower = content.strip().lower()
143
+
144
+ # Check for common diagram types
145
+ if content_lower.startswith(("graph ", "flowchart ")):
146
+ return "Flowchart"
147
+ elif content_lower.startswith("sequencediagram"):
148
+ return "Sequence"
149
+ elif content_lower.startswith("pie"):
150
+ return "Pie Chart"
151
+ elif content_lower.startswith("gantt"):
152
+ return "Gantt Chart"
153
+ elif content_lower.startswith("classdiagram"):
154
+ return "Class Diagram"
155
+ elif content_lower.startswith("statediagram"):
156
+ return "State Diagram"
157
+ elif content_lower.startswith("erdiagram"):
158
+ return "ER Diagram"
159
+ elif content_lower.startswith("journey"):
160
+ return "User Journey"
161
+ elif content_lower.startswith("gitgraph"):
162
+ return "Git Graph"
163
+ elif content_lower.startswith("c4context"):
164
+ return "C4 Context"
165
+ elif content_lower.startswith("mindmap"):
166
+ return "Mind Map"
167
+ elif content_lower.startswith("timeline"):
168
+ return "Timeline"
169
+ else:
170
+ return "Diagram"