pyagent-patterns 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- pyagent_patterns-0.2.0/.gitignore +45 -0
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/PKG-INFO +7 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/pyproject.toml +11 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/__init__.py +1 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/human_in_the_loop.py +1 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/react.py +2 -2
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/swarm.py +8 -8
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advisor.py +21 -14
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/base.py +6 -3
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/composite.py +8 -7
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/guardrails.py +2 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/__init__.py +1 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/fan_out_fan_in.py +5 -3
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/hierarchical.py +1 -3
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/orchestrator_workers.py +1 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/pipeline.py +6 -3
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/recovery.py +13 -10
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/registry.py +3 -2
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/__init__.py +1 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/cross_reflection.py +1 -0
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/debate.py +9 -6
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/evaluator_optimizer.py +1 -0
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/self_reflection.py +1 -0
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/voting.py +7 -7
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/streaming.py +15 -12
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/__init__.py +1 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/blackboard.py +1 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/layered.py +5 -8
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/role_based.py +1 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/topology.py +16 -12
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_advanced.py +1 -2
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_composite.py +6 -5
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_orchestration.py +13 -10
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_resolution.py +17 -7
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_structural.py +7 -6
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_valueadd.py +10 -6
- pyagent_patterns-0.1.0/.gitignore +0 -18
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/README.md +0 -0
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/__init__.py +1 -1
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/talker_reasoner.py +0 -0
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/supervisor.py +0 -0
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/py.typed +0 -0
- {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.eggs/
|
|
11
|
+
*.whl
|
|
12
|
+
|
|
13
|
+
# uv
|
|
14
|
+
.uv/
|
|
15
|
+
.venv/
|
|
16
|
+
uv.lock
|
|
17
|
+
|
|
18
|
+
# Environments
|
|
19
|
+
.env
|
|
20
|
+
.envrc
|
|
21
|
+
*.env
|
|
22
|
+
|
|
23
|
+
# Testing
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.coverage
|
|
26
|
+
htmlcov/
|
|
27
|
+
.tox/
|
|
28
|
+
|
|
29
|
+
# Type checking
|
|
30
|
+
.mypy_cache/
|
|
31
|
+
.dmypy.json
|
|
32
|
+
|
|
33
|
+
# Docs
|
|
34
|
+
site/
|
|
35
|
+
.mkdocs_temp/
|
|
36
|
+
|
|
37
|
+
# Editors
|
|
38
|
+
.vscode/
|
|
39
|
+
.idea/
|
|
40
|
+
*.swp
|
|
41
|
+
*.swo
|
|
42
|
+
|
|
43
|
+
# OS
|
|
44
|
+
.DS_Store
|
|
45
|
+
Thumbs.db
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyagent-patterns
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: 18 reusable multi-agent orchestration patterns for LLMs
|
|
5
|
+
Project-URL: Homepage, https://github.com/pyagent-core/pyagent
|
|
6
|
+
Project-URL: Source, https://github.com/pyagent-core/pyagent/tree/main/packages/pyagent-patterns
|
|
7
|
+
Project-URL: Documentation, https://pyagent-core.github.io/pyagent
|
|
8
|
+
Project-URL: Issues, https://github.com/pyagent-core/pyagent/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/pyagent-core/pyagent/blob/main/CHANGELOG.md
|
|
10
|
+
Author: PyAgent Contributors
|
|
5
11
|
License: MIT
|
|
6
12
|
Keywords: LLM,agents,multi-agent,orchestration,patterns
|
|
7
13
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyagent-patterns"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "18 reusable multi-agent orchestration patterns for LLMs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -20,6 +20,16 @@ classifiers = [
|
|
|
20
20
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
21
|
"Typing :: Typed",
|
|
22
22
|
]
|
|
23
|
+
authors = [
|
|
24
|
+
{name = "PyAgent Contributors"},
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/pyagent-core/pyagent"
|
|
29
|
+
Source = "https://github.com/pyagent-core/pyagent/tree/main/packages/pyagent-patterns"
|
|
30
|
+
Documentation = "https://pyagent-core.github.io/pyagent"
|
|
31
|
+
Issues = "https://github.com/pyagent-core/pyagent/issues"
|
|
32
|
+
Changelog = "https://github.com/pyagent-core/pyagent/blob/main/CHANGELOG.md"
|
|
23
33
|
|
|
24
34
|
[project.optional-dependencies]
|
|
25
35
|
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "ruff>=0.5", "mypy>=1.10"]
|
|
@@ -5,4 +5,4 @@ from pyagent_patterns.advanced.react import ReAct
|
|
|
5
5
|
from pyagent_patterns.advanced.swarm import Swarm
|
|
6
6
|
from pyagent_patterns.advanced.talker_reasoner import TalkerReasoner
|
|
7
7
|
|
|
8
|
-
__all__ = ["
|
|
8
|
+
__all__ = ["HumanInTheLoop", "ReAct", "Swarm", "TalkerReasoner"]
|
|
@@ -9,11 +9,11 @@ LLM calls: 1 per step × max_steps
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any
|
|
13
14
|
|
|
14
15
|
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
15
16
|
|
|
16
|
-
|
|
17
17
|
# Tool function type: (action_input: str) -> str
|
|
18
18
|
ToolFn = Callable[[str], str]
|
|
19
19
|
|
|
@@ -51,32 +51,32 @@ class Swarm(Pattern):
|
|
|
51
51
|
# Round 0: each agent independently responds to the task
|
|
52
52
|
init_tasks = [agent.run([Message.user(ctx.task)]) for agent in self._agents]
|
|
53
53
|
init_results = await asyncio.gather(*init_tasks)
|
|
54
|
-
for agent, result in zip(self._agents, init_results):
|
|
54
|
+
for agent, result in zip(self._agents, init_results, strict=False):
|
|
55
55
|
states[agent.name] = result.content
|
|
56
56
|
messages.append(result)
|
|
57
57
|
|
|
58
58
|
# Subsequent rounds: agents interact with random neighbors
|
|
59
|
-
for
|
|
59
|
+
for _round_num in range(1, self._rounds + 1):
|
|
60
60
|
new_states: dict[str, str] = {}
|
|
61
61
|
|
|
62
|
-
async def _update_agent(
|
|
62
|
+
async def _update_agent(
|
|
63
|
+
agent: Agent, _states: dict[str, str] = states
|
|
64
|
+
) -> tuple[str, str]:
|
|
63
65
|
# Select random neighbors
|
|
64
66
|
others = [a for a in self._agents if a.name != agent.name]
|
|
65
67
|
neighbors = random.sample(others, min(self._neighbor_count, len(others)))
|
|
66
68
|
|
|
67
|
-
neighbor_views = "\n".join(
|
|
68
|
-
f"- {n.name}: {states[n.name]}" for n in neighbors
|
|
69
|
-
)
|
|
69
|
+
neighbor_views = "\n".join(f"- {n.name}: {_states[n.name]}" for n in neighbors)
|
|
70
70
|
prompt = Message.user(
|
|
71
71
|
f"Task: {ctx.task}\n\n"
|
|
72
|
-
f"Your current response: {
|
|
72
|
+
f"Your current response: {_states[agent.name]}\n\n"
|
|
73
73
|
f"Neighbor responses:\n{neighbor_views}\n\n"
|
|
74
74
|
f"Update your response considering your neighbors' views."
|
|
75
75
|
)
|
|
76
76
|
result = await agent.run([prompt])
|
|
77
77
|
return agent.name, result.content
|
|
78
78
|
|
|
79
|
-
update_tasks = [_update_agent(agent) for agent in self._agents]
|
|
79
|
+
update_tasks = [_update_agent(agent, states) for agent in self._agents]
|
|
80
80
|
updates = await asyncio.gather(*update_tasks)
|
|
81
81
|
for name, content in updates:
|
|
82
82
|
new_states[name] = content
|
|
@@ -13,17 +13,17 @@ Decision factors:
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
from dataclasses import dataclass
|
|
16
|
-
from enum import
|
|
16
|
+
from enum import StrEnum
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class Quality(
|
|
19
|
+
class Quality(StrEnum):
|
|
20
20
|
DRAFT = "draft"
|
|
21
21
|
STANDARD = "standard"
|
|
22
22
|
HIGH = "high"
|
|
23
23
|
CRITICAL = "critical"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class Latency(
|
|
26
|
+
class Latency(StrEnum):
|
|
27
27
|
REALTIME = "realtime" # < 2s
|
|
28
28
|
INTERACTIVE = "interactive" # < 10s
|
|
29
29
|
BATCH = "batch" # minutes OK
|
|
@@ -68,15 +68,18 @@ class PatternAdvisor:
|
|
|
68
68
|
# Decision tree based on Augment 2026 "Five Decision Rules"
|
|
69
69
|
|
|
70
70
|
# Rule 1: Simple single-step tasks → Pipeline or single agent
|
|
71
|
-
if
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
if (
|
|
72
|
+
not c.multi_step
|
|
73
|
+
and c.quality in (Quality.DRAFT, Quality.STANDARD)
|
|
74
|
+
and c.latency == Latency.REALTIME
|
|
75
|
+
):
|
|
76
|
+
return Recommendation(
|
|
77
|
+
pattern="pipeline",
|
|
78
|
+
reason="Simple task with real-time latency → minimal sequential processing",
|
|
79
|
+
estimated_calls=1,
|
|
80
|
+
estimated_cost_range="$0.001-0.003",
|
|
81
|
+
alternatives=["talker_reasoner"],
|
|
82
|
+
)
|
|
80
83
|
|
|
81
84
|
# Rule 2: Cost-sensitive → Route to cheapest viable model
|
|
82
85
|
if c.max_cost_usd < 0.01:
|
|
@@ -101,7 +104,9 @@ class PatternAdvisor:
|
|
|
101
104
|
# Rule 4: High quality → Reflection, Debate, or Evaluator
|
|
102
105
|
if c.quality in (Quality.HIGH, Quality.CRITICAL):
|
|
103
106
|
# Check for adversarial/debate keywords
|
|
104
|
-
if any(
|
|
107
|
+
if any(
|
|
108
|
+
w in task_lower for w in ["compare", "pros and cons", "debate", "argue", "versus"]
|
|
109
|
+
):
|
|
105
110
|
return Recommendation(
|
|
106
111
|
pattern="debate",
|
|
107
112
|
reason="High quality + adversarial task → structured debate with judge",
|
|
@@ -129,7 +134,9 @@ class PatternAdvisor:
|
|
|
129
134
|
)
|
|
130
135
|
|
|
131
136
|
# Rule 5: Multi-step/complex → Supervisor, Hierarchical, or Pipeline
|
|
132
|
-
if c.multi_step or any(
|
|
137
|
+
if c.multi_step or any(
|
|
138
|
+
w in task_lower for w in ["steps", "process", "workflow", "pipeline"]
|
|
139
|
+
):
|
|
133
140
|
if any(w in task_lower for w in ["team", "delegate", "manage", "coordinate"]):
|
|
134
141
|
return Recommendation(
|
|
135
142
|
pattern="hierarchical",
|
|
@@ -7,11 +7,14 @@ import time
|
|
|
7
7
|
import uuid
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
|
-
from enum import
|
|
11
|
-
from typing import
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
12
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
|
|
17
|
+
class Role(StrEnum):
|
|
15
18
|
"""Well-known agent roles."""
|
|
16
19
|
|
|
17
20
|
SYSTEM = "system"
|
|
@@ -8,11 +8,10 @@ EscalationChain is a pre-built composite: Reflection → Debate → Voting → H
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from collections.abc import Callable
|
|
12
12
|
|
|
13
13
|
from pyagent_patterns.base import Context, Pattern, Result
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
# Quality check: returns True if the result is acceptable
|
|
17
16
|
QualityCheckFn = Callable[[Result], bool]
|
|
18
17
|
|
|
@@ -68,11 +67,13 @@ class CompositePattern(Pattern):
|
|
|
68
67
|
|
|
69
68
|
result = await pattern._execute(child_ctx)
|
|
70
69
|
all_messages.extend(result.messages)
|
|
71
|
-
escalation_log.append(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
escalation_log.append(
|
|
71
|
+
{
|
|
72
|
+
"pattern": pattern.pattern_type,
|
|
73
|
+
"output_length": len(result.output),
|
|
74
|
+
"metadata": result.metadata,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
76
77
|
|
|
77
78
|
if self._quality_check(result):
|
|
78
79
|
return Result(
|
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
import re
|
|
16
16
|
from abc import ABC, abstractmethod
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
+
from typing import ClassVar
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@dataclass
|
|
@@ -71,7 +72,7 @@ class PIIGuard(Guardrail):
|
|
|
71
72
|
redact: If True, redact PII and pass. If False, reject on detection.
|
|
72
73
|
"""
|
|
73
74
|
|
|
74
|
-
_PATTERNS = {
|
|
75
|
+
_PATTERNS: ClassVar[dict[str, str]] = {
|
|
75
76
|
"email": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
|
|
76
77
|
"phone": r"\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b",
|
|
77
78
|
"ssn": r"\b\d{3}[-]?\d{2}[-]?\d{4}\b",
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/__init__.py
RENAMED
|
@@ -6,4 +6,4 @@ from pyagent_patterns.orchestration.orchestrator_workers import OrchestratorWork
|
|
|
6
6
|
from pyagent_patterns.orchestration.pipeline import Pipeline
|
|
7
7
|
from pyagent_patterns.orchestration.supervisor import Supervisor
|
|
8
8
|
|
|
9
|
-
__all__ = ["
|
|
9
|
+
__all__ = ["FanOutFanIn", "Hierarchical", "OrchestratorWorkers", "Pipeline", "Supervisor"]
|
|
@@ -10,10 +10,13 @@ Wall-clock latency: max(agent latencies) + aggregator
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
14
|
|
|
15
15
|
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
16
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import AsyncIterator
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
class FanOutFanIn(Pattern):
|
|
19
22
|
"""Parallel execution with result aggregation.
|
|
@@ -43,8 +46,7 @@ class FanOutFanIn(Pattern):
|
|
|
43
46
|
|
|
44
47
|
# Fan-in: aggregate results
|
|
45
48
|
combined = "\n\n".join(
|
|
46
|
-
f"--- {self._agents[i].name} ---\n{r.content}"
|
|
47
|
-
for i, r in enumerate(parallel_results)
|
|
49
|
+
f"--- {self._agents[i].name} ---\n{r.content}" for i, r in enumerate(parallel_results)
|
|
48
50
|
)
|
|
49
51
|
agg_prompt = Message.user(
|
|
50
52
|
f"Combine the following analyses into a unified response:\n\n{combined}"
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/hierarchical.py
RENAMED
|
@@ -75,9 +75,7 @@ class Hierarchical(Pattern):
|
|
|
75
75
|
worker_summary = "\n".join(
|
|
76
76
|
f"- {team.workers[i].name}: {r.content}" for i, r in enumerate(worker_results)
|
|
77
77
|
)
|
|
78
|
-
lead_msg = Message.user(
|
|
79
|
-
f"Synthesize your team's work:\n{worker_summary}"
|
|
80
|
-
)
|
|
78
|
+
lead_msg = Message.user(f"Synthesize your team's work:\n{worker_summary}")
|
|
81
79
|
lead_result = await team.lead.run([lead_msg])
|
|
82
80
|
team_msgs.append(lead_result)
|
|
83
81
|
return lead_result.content, team_msgs
|
|
@@ -38,7 +38,7 @@ class OrchestratorWorkers(Pattern):
|
|
|
38
38
|
plan_prompt = Message.user(
|
|
39
39
|
f"You have these workers available: {', '.join(worker_names)}.\n"
|
|
40
40
|
f"Plan how to accomplish this task by assigning subtasks to workers.\n"
|
|
41
|
-
f
|
|
41
|
+
f'Respond as JSON: {{"assignments": [{{"worker": "name", "subtask": "description"}}]}}\n\n'
|
|
42
42
|
f"Task: {ctx.task}"
|
|
43
43
|
)
|
|
44
44
|
plan_msg = await self._orchestrator.run([plan_prompt])
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/pipeline.py
RENAMED
|
@@ -8,10 +8,13 @@ LLM calls: N (one per stage)
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
from typing import
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
13
|
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
14
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import AsyncIterator
|
|
17
|
+
|
|
15
18
|
|
|
16
19
|
class Pipeline(Pattern):
|
|
17
20
|
"""Sequential stage chain — output of stage N becomes input of stage N+1.
|
|
@@ -33,7 +36,7 @@ class Pipeline(Pattern):
|
|
|
33
36
|
messages: list[Message] = []
|
|
34
37
|
current_input = ctx.task
|
|
35
38
|
|
|
36
|
-
for
|
|
39
|
+
for _i, stage in enumerate(self._stages):
|
|
37
40
|
stage_msg = Message.user(current_input)
|
|
38
41
|
response = await stage.run([stage_msg])
|
|
39
42
|
messages.append(response)
|
|
@@ -47,7 +50,7 @@ class Pipeline(Pattern):
|
|
|
47
50
|
|
|
48
51
|
async def stream(self, task: str, context: Context | None = None) -> AsyncIterator[str]:
|
|
49
52
|
"""Stream stage completions as they finish."""
|
|
50
|
-
|
|
53
|
+
context or Context(task=task)
|
|
51
54
|
current_input = task
|
|
52
55
|
|
|
53
56
|
for i, stage in enumerate(self._stages):
|
|
@@ -11,14 +11,13 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import time
|
|
14
|
-
from dataclasses import dataclass
|
|
15
|
-
from enum import
|
|
16
|
-
from typing import Any
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import StrEnum
|
|
17
16
|
|
|
18
17
|
from pyagent_patterns.base import Context, Pattern, Result
|
|
19
18
|
|
|
20
19
|
|
|
21
|
-
class CircuitState(
|
|
20
|
+
class CircuitState(StrEnum):
|
|
22
21
|
CLOSED = "closed" # Normal operation
|
|
23
22
|
OPEN = "open" # Failing, rejecting requests
|
|
24
23
|
HALF_OPEN = "half_open" # Testing if recovered
|
|
@@ -69,10 +68,12 @@ class BoundedExecution:
|
|
|
69
68
|
result.metadata["attempts"] = attempt
|
|
70
69
|
return result
|
|
71
70
|
# Token limit exceeded — try next level
|
|
72
|
-
last_error = Exception(
|
|
71
|
+
last_error = Exception(
|
|
72
|
+
f"Token limit exceeded: {result.token_estimate}/{self.max_tokens}"
|
|
73
|
+
)
|
|
73
74
|
break
|
|
74
|
-
except
|
|
75
|
-
last_error =
|
|
75
|
+
except TimeoutError:
|
|
76
|
+
last_error = TimeoutError(f"Timeout after {self.timeout_seconds}s")
|
|
76
77
|
break
|
|
77
78
|
except Exception as e:
|
|
78
79
|
last_error = e
|
|
@@ -131,9 +132,11 @@ class CircuitBreaker:
|
|
|
131
132
|
|
|
132
133
|
@property
|
|
133
134
|
def state(self) -> CircuitState:
|
|
134
|
-
if
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
if (
|
|
136
|
+
self._state == CircuitState.OPEN
|
|
137
|
+
and time.time() - self._last_failure_time > self._reset_timeout
|
|
138
|
+
):
|
|
139
|
+
self._state = CircuitState.HALF_OPEN
|
|
137
140
|
return self._state
|
|
138
141
|
|
|
139
142
|
async def execute(self, pattern: Pattern, task: str, context: Context | None = None) -> Result:
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pyagent_patterns.base import Pattern
|
|
8
9
|
|
|
9
10
|
_REGISTRY: dict[str, type[Pattern]] = {}
|
|
10
11
|
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/__init__.py
RENAMED
|
@@ -6,4 +6,4 @@ from pyagent_patterns.resolution.evaluator_optimizer import EvaluatorOptimizer
|
|
|
6
6
|
from pyagent_patterns.resolution.self_reflection import SelfReflection
|
|
7
7
|
from pyagent_patterns.resolution.voting import Voting
|
|
8
8
|
|
|
9
|
-
__all__ = ["
|
|
9
|
+
__all__ = ["CrossReflection", "Debate", "EvaluatorOptimizer", "SelfReflection", "Voting"]
|
|
@@ -43,6 +43,7 @@ class Debate(Pattern):
|
|
|
43
43
|
async def _execute(self, ctx: Context) -> Result:
|
|
44
44
|
messages: list[Message] = []
|
|
45
45
|
debate_log: list[dict[str, str]] = []
|
|
46
|
+
round_args_prev: list[str] = []
|
|
46
47
|
|
|
47
48
|
for round_num in range(1, self._rounds + 1):
|
|
48
49
|
round_args: list[str] = []
|
|
@@ -69,12 +70,14 @@ class Debate(Pattern):
|
|
|
69
70
|
arg_result = await debater.run([prompt])
|
|
70
71
|
messages.append(arg_result)
|
|
71
72
|
round_args.append(arg_result.content)
|
|
72
|
-
debate_log.append(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
debate_log.append(
|
|
74
|
+
{
|
|
75
|
+
"round": round_num,
|
|
76
|
+
"debater": debater.name,
|
|
77
|
+
"position": position,
|
|
78
|
+
"argument": arg_result.content,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
78
81
|
|
|
79
82
|
round_args_prev = round_args
|
|
80
83
|
|
|
@@ -10,12 +10,12 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
12
|
from collections import Counter
|
|
13
|
-
from enum import
|
|
13
|
+
from enum import StrEnum
|
|
14
14
|
|
|
15
15
|
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class VotingStrategy(
|
|
18
|
+
class VotingStrategy(StrEnum):
|
|
19
19
|
MAJORITY = "majority"
|
|
20
20
|
WEIGHTED = "weighted"
|
|
21
21
|
|
|
@@ -55,12 +55,12 @@ class Voting(Pattern):
|
|
|
55
55
|
|
|
56
56
|
suffix = ""
|
|
57
57
|
if self._normalize:
|
|
58
|
-
suffix =
|
|
58
|
+
suffix = (
|
|
59
|
+
"\n\nRespond with a concise answer (one word or short phrase) first, then explain."
|
|
60
|
+
)
|
|
59
61
|
|
|
60
62
|
# All voters run in parallel
|
|
61
|
-
tasks = [
|
|
62
|
-
voter.run([Message.user(ctx.task + suffix)]) for voter in self._voters
|
|
63
|
-
]
|
|
63
|
+
tasks = [voter.run([Message.user(ctx.task + suffix)]) for voter in self._voters]
|
|
64
64
|
votes = await asyncio.gather(*tasks)
|
|
65
65
|
messages.extend(votes)
|
|
66
66
|
|
|
@@ -75,7 +75,7 @@ class Voting(Pattern):
|
|
|
75
75
|
else:
|
|
76
76
|
# Weighted voting
|
|
77
77
|
weighted_counts: dict[str, float] = {}
|
|
78
|
-
for label, weight in zip(vote_labels, self._weights):
|
|
78
|
+
for label, weight in zip(vote_labels, self._weights, strict=False):
|
|
79
79
|
weighted_counts[label] = weighted_counts.get(label, 0.0) + weight
|
|
80
80
|
winner = max(weighted_counts, key=weighted_counts.get) # type: ignore[arg-type]
|
|
81
81
|
tally = weighted_counts
|
|
@@ -12,9 +12,12 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
14
|
from dataclasses import dataclass, field
|
|
15
|
-
from typing import
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import AsyncIterator
|
|
19
|
+
|
|
20
|
+
from pyagent_patterns.base import Pattern
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
@dataclass
|
|
@@ -36,8 +39,8 @@ async def stream_pattern(pattern: Pattern, task: str) -> AsyncIterator[StreamChu
|
|
|
36
39
|
For Pipeline patterns, yields after each stage completes.
|
|
37
40
|
For Fan-Out, yields as each parallel agent completes.
|
|
38
41
|
"""
|
|
39
|
-
from pyagent_patterns.orchestration.pipeline import Pipeline
|
|
40
42
|
from pyagent_patterns.orchestration.fan_out_fan_in import FanOutFanIn
|
|
43
|
+
from pyagent_patterns.orchestration.pipeline import Pipeline
|
|
41
44
|
|
|
42
45
|
if isinstance(pattern, Pipeline):
|
|
43
46
|
async for chunk in _stream_pipeline(pattern, task):
|
|
@@ -87,12 +90,14 @@ async def _stream_fanout(fanout: Pattern, task: str) -> AsyncIterator[StreamChun
|
|
|
87
90
|
if agent.system_prompt:
|
|
88
91
|
messages.insert(0, Message(role=Role.SYSTEM, content=agent.system_prompt))
|
|
89
92
|
response = await agent.llm.generate(messages)
|
|
90
|
-
await queue.put(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
await queue.put(
|
|
94
|
+
StreamChunk(
|
|
95
|
+
content=response,
|
|
96
|
+
agent_name=agent.name,
|
|
97
|
+
chunk_type="stage_complete",
|
|
98
|
+
metadata={"agent_index": idx},
|
|
99
|
+
)
|
|
100
|
+
)
|
|
96
101
|
|
|
97
102
|
tasks = [asyncio.create_task(run_agent(a, i)) for i, a in enumerate(fanout._agents)]
|
|
98
103
|
done_count = 0
|
|
@@ -107,9 +112,7 @@ async def _stream_fanout(fanout: Pattern, task: str) -> AsyncIterator[StreamChun
|
|
|
107
112
|
await asyncio.gather(*tasks)
|
|
108
113
|
|
|
109
114
|
# Aggregate
|
|
110
|
-
|
|
111
|
-
f"[{a.name}]: (see prior chunks)" for a in fanout._agents
|
|
112
|
-
)
|
|
115
|
+
"\n".join(f"[{a.name}]: (see prior chunks)" for a in fanout._agents)
|
|
113
116
|
result = await fanout.run(task)
|
|
114
117
|
yield StreamChunk(
|
|
115
118
|
content=result.output,
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/__init__.py
RENAMED
|
@@ -5,4 +5,4 @@ from pyagent_patterns.structural.layered import Layered
|
|
|
5
5
|
from pyagent_patterns.structural.role_based import RoleBased
|
|
6
6
|
from pyagent_patterns.structural.topology import Topology, TopologyType
|
|
7
7
|
|
|
8
|
-
__all__ = ["
|
|
8
|
+
__all__ = ["Blackboard", "Layered", "RoleBased", "Topology", "TopologyType"]
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/blackboard.py
RENAMED
|
@@ -103,7 +103,7 @@ class Blackboard(Pattern):
|
|
|
103
103
|
for key, value in self._initial_state.items():
|
|
104
104
|
board.write(key, value, "system")
|
|
105
105
|
|
|
106
|
-
for
|
|
106
|
+
for _round_num in range(1, self._rounds + 1):
|
|
107
107
|
for ba in self._agents:
|
|
108
108
|
# Build prompt from readable keys
|
|
109
109
|
readable = {k: board.read(k) for k in ba.reads if board.read(k) is not None}
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/layered.py
RENAMED
|
@@ -42,11 +42,9 @@ class Layered(Pattern):
|
|
|
42
42
|
messages: list[Message] = []
|
|
43
43
|
layer_input = ctx.task
|
|
44
44
|
|
|
45
|
-
for
|
|
45
|
+
for _i, layer in enumerate(self._layers):
|
|
46
46
|
# Run all agents in this layer in parallel
|
|
47
|
-
tasks = [
|
|
48
|
-
agent.run([Message.user(layer_input)]) for agent in layer.agents
|
|
49
|
-
]
|
|
47
|
+
tasks = [agent.run([Message.user(layer_input)]) for agent in layer.agents]
|
|
50
48
|
layer_results = await asyncio.gather(*tasks)
|
|
51
49
|
messages.extend(layer_results)
|
|
52
50
|
|
|
@@ -55,8 +53,7 @@ class Layered(Pattern):
|
|
|
55
53
|
layer_input = layer_results[0].content
|
|
56
54
|
else:
|
|
57
55
|
layer_input = "\n\n".join(
|
|
58
|
-
f"[{layer.agents[j].name}]: {r.content}"
|
|
59
|
-
for j, r in enumerate(layer_results)
|
|
56
|
+
f"[{layer.agents[j].name}]: {r.content}" for j, r in enumerate(layer_results)
|
|
60
57
|
)
|
|
61
58
|
|
|
62
59
|
return Result(
|
|
@@ -64,7 +61,7 @@ class Layered(Pattern):
|
|
|
64
61
|
messages=messages,
|
|
65
62
|
metadata={
|
|
66
63
|
"layer_count": len(self._layers),
|
|
67
|
-
"layer_names": [
|
|
68
|
-
"agents_per_layer": [len(
|
|
64
|
+
"layer_names": [layer_.name for layer_ in self._layers],
|
|
65
|
+
"agents_per_layer": [len(layer_.agents) for layer_ in self._layers],
|
|
69
66
|
},
|
|
70
67
|
)
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/role_based.py
RENAMED
|
@@ -39,7 +39,7 @@ class RoleBased(Pattern):
|
|
|
39
39
|
messages: list[Message] = []
|
|
40
40
|
conversation: list[Message] = [Message.user(ctx.task)]
|
|
41
41
|
|
|
42
|
-
for
|
|
42
|
+
for _round_num in range(1, self._rounds + 1):
|
|
43
43
|
for agent in self._agents:
|
|
44
44
|
if self._shared_context:
|
|
45
45
|
input_msgs = list(conversation)
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/topology.py
RENAMED
|
@@ -11,12 +11,12 @@ flow, overhead, and result quality.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
|
-
from enum import
|
|
14
|
+
from enum import StrEnum
|
|
15
15
|
|
|
16
16
|
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class TopologyType(
|
|
19
|
+
class TopologyType(StrEnum):
|
|
20
20
|
CHAIN = "chain"
|
|
21
21
|
STAR = "star"
|
|
22
22
|
MESH = "mesh"
|
|
@@ -82,12 +82,10 @@ class Topology(Pattern):
|
|
|
82
82
|
messages.extend(spoke_results)
|
|
83
83
|
|
|
84
84
|
# Hub synthesizes
|
|
85
|
-
summary = "\n".join(
|
|
86
|
-
|
|
85
|
+
summary = "\n".join(f"- {spokes[i].name}: {r.content}" for i, r in enumerate(spoke_results))
|
|
86
|
+
hub_result = await hub.run(
|
|
87
|
+
[Message.user(f"Synthesize these inputs:\n{summary}\n\nOriginal task: {ctx.task}")]
|
|
87
88
|
)
|
|
88
|
-
hub_result = await hub.run([Message.user(
|
|
89
|
-
f"Synthesize these inputs:\n{summary}\n\nOriginal task: {ctx.task}"
|
|
90
|
-
)])
|
|
91
89
|
messages.append(hub_result)
|
|
92
90
|
|
|
93
91
|
return Result(output=hub_result.content, messages=messages, metadata={"topology": "star"})
|
|
@@ -100,7 +98,7 @@ class Topology(Pattern):
|
|
|
100
98
|
# Initial round
|
|
101
99
|
tasks = [a.run([Message.user(ctx.task)]) for a in self._agents]
|
|
102
100
|
initial = await asyncio.gather(*tasks)
|
|
103
|
-
for agent, result in zip(self._agents, initial):
|
|
101
|
+
for agent, result in zip(self._agents, initial, strict=False):
|
|
104
102
|
outputs[agent.name] = result.content
|
|
105
103
|
messages.append(result)
|
|
106
104
|
|
|
@@ -112,12 +110,18 @@ class Topology(Pattern):
|
|
|
112
110
|
for name, content in outputs.items()
|
|
113
111
|
if name != agent.name
|
|
114
112
|
)
|
|
115
|
-
result = await agent.run(
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
result = await agent.run(
|
|
114
|
+
[
|
|
115
|
+
Message.user(
|
|
116
|
+
f"Task: {ctx.task}\n\nPeer outputs:\n{peer_outputs}\n\nProvide your updated response."
|
|
117
|
+
)
|
|
118
|
+
]
|
|
119
|
+
)
|
|
118
120
|
outputs[agent.name] = result.content
|
|
119
121
|
messages.append(result)
|
|
120
122
|
|
|
121
123
|
# Final output is concatenation of all agent outputs
|
|
122
124
|
final = "\n\n".join(f"[{name}]: {content}" for name, content in outputs.items())
|
|
123
|
-
return Result(
|
|
125
|
+
return Result(
|
|
126
|
+
output=final, messages=messages, metadata={"topology": "mesh", "rounds": self._rounds}
|
|
127
|
+
)
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
-
|
|
7
|
-
from pyagent_patterns.base import Agent, MockLLM
|
|
8
6
|
from pyagent_patterns.advanced import HumanInTheLoop, ReAct, Swarm, TalkerReasoner
|
|
9
7
|
from pyagent_patterns.advanced.human_in_the_loop import HumanDecision
|
|
8
|
+
from pyagent_patterns.base import Agent, MockLLM
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
@pytest.mark.asyncio
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
-
|
|
7
6
|
from pyagent_patterns.base import Agent, MockLLM
|
|
8
7
|
from pyagent_patterns.composite import CompositePattern, min_length_check
|
|
9
8
|
from pyagent_patterns.resolution import SelfReflection, Voting
|
|
@@ -32,10 +31,12 @@ async def test_composite_escalation():
|
|
|
32
31
|
reflection = SelfReflection(agent=Agent("coder", short_llm), max_rounds=1)
|
|
33
32
|
|
|
34
33
|
# Second pattern produces adequate output
|
|
35
|
-
vote_llm = MockLLM(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
vote_llm = MockLLM(
|
|
35
|
+
responses=[
|
|
36
|
+
"A much longer and more detailed response that passes the length check",
|
|
37
|
+
"A much longer and more detailed response that passes the length check",
|
|
38
|
+
]
|
|
39
|
+
)
|
|
39
40
|
voting = Voting(voters=[Agent("a", vote_llm), Agent("b", vote_llm)])
|
|
40
41
|
|
|
41
42
|
composite = CompositePattern(
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
-
|
|
7
6
|
from pyagent_patterns.base import Agent, MockLLM
|
|
8
7
|
from pyagent_patterns.orchestration import (
|
|
9
8
|
FanOutFanIn,
|
|
@@ -39,11 +38,13 @@ async def test_supervisor_routes_correctly():
|
|
|
39
38
|
async def test_pipeline_sequential_stages():
|
|
40
39
|
stage_llm = MockLLM(responses=["extracted text", "summarized text", "translated text"])
|
|
41
40
|
|
|
42
|
-
pipeline = Pipeline(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
pipeline = Pipeline(
|
|
42
|
+
stages=[
|
|
43
|
+
Agent("extract", stage_llm),
|
|
44
|
+
Agent("summarize", stage_llm),
|
|
45
|
+
Agent("translate", stage_llm),
|
|
46
|
+
]
|
|
47
|
+
)
|
|
47
48
|
|
|
48
49
|
result = await pipeline.run("Process this document")
|
|
49
50
|
assert result.metadata["stages"] == 3
|
|
@@ -90,10 +91,12 @@ async def test_hierarchical_delegation():
|
|
|
90
91
|
|
|
91
92
|
@pytest.mark.asyncio
|
|
92
93
|
async def test_orchestrator_workers_dynamic():
|
|
93
|
-
orch_llm = MockLLM(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
orch_llm = MockLLM(
|
|
95
|
+
responses=[
|
|
96
|
+
'{"assignments": [{"worker": "researcher", "subtask": "Find data"}]}',
|
|
97
|
+
"Synthesized: research complete",
|
|
98
|
+
]
|
|
99
|
+
)
|
|
97
100
|
worker_llm = MockLLM(responses=["Data found on topic X"])
|
|
98
101
|
|
|
99
102
|
orch = OrchestratorWorkers(
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
-
|
|
7
6
|
from pyagent_patterns.base import Agent, MockLLM
|
|
8
7
|
from pyagent_patterns.resolution import (
|
|
9
8
|
CrossReflection,
|
|
@@ -25,7 +24,16 @@ async def test_self_reflection_early_stop():
|
|
|
25
24
|
|
|
26
25
|
@pytest.mark.asyncio
|
|
27
26
|
async def test_self_reflection_max_rounds():
|
|
28
|
-
llm = MockLLM(
|
|
27
|
+
llm = MockLLM(
|
|
28
|
+
responses=[
|
|
29
|
+
"Draft 1",
|
|
30
|
+
"Needs work",
|
|
31
|
+
"Draft 2",
|
|
32
|
+
"Still needs work",
|
|
33
|
+
"Draft 3",
|
|
34
|
+
"More feedback",
|
|
35
|
+
]
|
|
36
|
+
)
|
|
29
37
|
pattern = SelfReflection(agent=Agent("coder", llm), max_rounds=3)
|
|
30
38
|
result = await pattern.run("Write code")
|
|
31
39
|
assert result.metadata["rounds"] == 3
|
|
@@ -69,11 +77,13 @@ async def test_voting_majority():
|
|
|
69
77
|
llm_b = MockLLM(responses=["YES\nBecause of Y"])
|
|
70
78
|
llm_c = MockLLM(responses=["NO\nBecause of Z"])
|
|
71
79
|
|
|
72
|
-
pattern = Voting(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
pattern = Voting(
|
|
81
|
+
voters=[
|
|
82
|
+
Agent("voter_a", llm_a),
|
|
83
|
+
Agent("voter_b", llm_b),
|
|
84
|
+
Agent("voter_c", llm_c),
|
|
85
|
+
]
|
|
86
|
+
)
|
|
77
87
|
result = await pattern.run("Is this a good idea?")
|
|
78
88
|
assert result.metadata["winner"] == "YES"
|
|
79
89
|
assert result.metadata["tally"]["YES"] == 2
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
-
|
|
7
6
|
from pyagent_patterns.base import Agent, MockLLM
|
|
8
7
|
from pyagent_patterns.structural import Blackboard, Layered, RoleBased, Topology, TopologyType
|
|
9
8
|
from pyagent_patterns.structural.blackboard import BlackboardAgent
|
|
@@ -29,11 +28,13 @@ async def test_role_based_shared_context():
|
|
|
29
28
|
async def test_layered_multi_layer():
|
|
30
29
|
llm = MockLLM(responses=["Raw data", "Analysis result", "Final synthesis"])
|
|
31
30
|
|
|
32
|
-
pattern = Layered(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
pattern = Layered(
|
|
32
|
+
layers=[
|
|
33
|
+
Layer(name="gather", agents=[Agent("gatherer", llm)]),
|
|
34
|
+
Layer(name="analyze", agents=[Agent("analyst", llm)]),
|
|
35
|
+
Layer(name="synthesize", agents=[Agent("synthesizer", llm)]),
|
|
36
|
+
]
|
|
37
|
+
)
|
|
37
38
|
result = await pattern.run("Analyze the market")
|
|
38
39
|
assert result.metadata["layer_count"] == 3
|
|
39
40
|
assert result.output == "Final synthesis"
|
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
-
|
|
7
6
|
from pyagent_patterns.advisor import Constraints, Latency, PatternAdvisor, Quality
|
|
8
7
|
from pyagent_patterns.base import Agent, MockLLM
|
|
9
8
|
from pyagent_patterns.guardrails import ContentGuard, GuardrailChain, LengthGuard, PIIGuard
|
|
10
9
|
from pyagent_patterns.orchestration import Pipeline
|
|
11
10
|
from pyagent_patterns.recovery import BoundedExecution, CircuitBreaker, CircuitState
|
|
12
11
|
|
|
13
|
-
|
|
14
12
|
# --- Recovery Tests ---
|
|
15
13
|
|
|
14
|
+
|
|
16
15
|
@pytest.mark.asyncio
|
|
17
16
|
async def test_bounded_execution_success():
|
|
18
17
|
llm = MockLLM(responses=["Success output"])
|
|
@@ -50,6 +49,7 @@ def test_circuit_breaker_resets():
|
|
|
50
49
|
cb._on_failure()
|
|
51
50
|
assert cb.state == CircuitState.OPEN
|
|
52
51
|
import time
|
|
52
|
+
|
|
53
53
|
time.sleep(0.02)
|
|
54
54
|
assert cb.state == CircuitState.HALF_OPEN
|
|
55
55
|
cb._on_success()
|
|
@@ -58,6 +58,7 @@ def test_circuit_breaker_resets():
|
|
|
58
58
|
|
|
59
59
|
# --- Guardrail Tests ---
|
|
60
60
|
|
|
61
|
+
|
|
61
62
|
def test_length_guard_pass():
|
|
62
63
|
guard = LengthGuard(max_chars=100)
|
|
63
64
|
result = guard.check("Short text")
|
|
@@ -98,10 +99,12 @@ def test_content_guard_deny_words():
|
|
|
98
99
|
|
|
99
100
|
|
|
100
101
|
def test_guardrail_chain():
|
|
101
|
-
chain = GuardrailChain(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
chain = GuardrailChain(
|
|
103
|
+
[
|
|
104
|
+
LengthGuard(max_chars=1000),
|
|
105
|
+
PIIGuard(redact=True),
|
|
106
|
+
]
|
|
107
|
+
)
|
|
105
108
|
result = chain.check("Email: test@test.com. This is fine otherwise.")
|
|
106
109
|
assert result.passed is True
|
|
107
110
|
assert "REDACTED" in result.sanitized_content
|
|
@@ -109,6 +112,7 @@ def test_guardrail_chain():
|
|
|
109
112
|
|
|
110
113
|
# --- Advisor Tests ---
|
|
111
114
|
|
|
115
|
+
|
|
112
116
|
def test_advisor_simple_task():
|
|
113
117
|
advisor = PatternAdvisor()
|
|
114
118
|
rec = advisor.recommend(
|
|
File without changes
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/talker_reasoner.py
RENAMED
|
File without changes
|
{pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/supervisor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|