flowai-cli 0.2.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.
- flowai/__init__.py +72 -0
- flowai/__main__.py +8 -0
- flowai/agents/__init__.py +21 -0
- flowai/agents/base.py +61 -0
- flowai/agents/patterns/__init__.py +11 -0
- flowai/agents/patterns/debate.py +79 -0
- flowai/agents/patterns/reflection.py +74 -0
- flowai/agents/specialized.py +87 -0
- flowai/agents/standard.py +176 -0
- flowai/cli/__init__.py +5 -0
- flowai/cli/main.py +562 -0
- flowai/cli/ui.py +98 -0
- flowai/config/__init__.py +5 -0
- flowai/config/settings.py +204 -0
- flowai/errors.py +30 -0
- flowai/mcp/__init__.py +5 -0
- flowai/mcp/client.py +77 -0
- flowai/memory/__init__.py +16 -0
- flowai/memory/base.py +45 -0
- flowai/memory/chroma.py +97 -0
- flowai/memory/in_memory.py +65 -0
- flowai/memory/qdrant.py +104 -0
- flowai/observability/__init__.py +15 -0
- flowai/observability/events.py +45 -0
- flowai/observability/opentelemetry.py +56 -0
- flowai/observability/tracer.py +80 -0
- flowai/plugins/__init__.py +5 -0
- flowai/plugins/loader.py +31 -0
- flowai/providers/__init__.py +35 -0
- flowai/providers/anthropic.py +268 -0
- flowai/providers/base.py +180 -0
- flowai/providers/gemini.py +230 -0
- flowai/providers/nvidia.py +57 -0
- flowai/providers/ollama.py +237 -0
- flowai/providers/openai.py +274 -0
- flowai/providers/registry.py +50 -0
- flowai/runtime/__init__.py +5 -0
- flowai/runtime/session.py +76 -0
- flowai/server/__init__.py +5 -0
- flowai/server/app.py +133 -0
- flowai/tools/__init__.py +39 -0
- flowai/tools/base.py +56 -0
- flowai/tools/builtin.py +128 -0
- flowai/tools/decorator.py +103 -0
- flowai/tools/files.py +197 -0
- flowai/tools/registry.py +84 -0
- flowai/tools/web.py +94 -0
- flowai/workflow/__init__.py +15 -0
- flowai/workflow/checkpoint.py +73 -0
- flowai/workflow/engine.py +144 -0
- flowai/workflow/state.py +43 -0
- flowai_cli-0.2.0.dist-info/METADATA +223 -0
- flowai_cli-0.2.0.dist-info/RECORD +56 -0
- flowai_cli-0.2.0.dist-info/WHEEL +4 -0
- flowai_cli-0.2.0.dist-info/entry_points.txt +2 -0
- flowai_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
flowai/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""FlowAI: An open-source, production-ready, extensible Python Agent Harness."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
|
|
5
|
+
from flowai.agents import (
|
|
6
|
+
Agent,
|
|
7
|
+
AssistantAgent,
|
|
8
|
+
CriticAgent,
|
|
9
|
+
DebatePattern,
|
|
10
|
+
PlanAndExecutePattern,
|
|
11
|
+
PlannerAgent,
|
|
12
|
+
ReflectionPattern,
|
|
13
|
+
SupervisorAgent,
|
|
14
|
+
Swarm,
|
|
15
|
+
ToolAgent,
|
|
16
|
+
)
|
|
17
|
+
from flowai.config import Settings
|
|
18
|
+
from flowai.memory import (
|
|
19
|
+
BaseMemory,
|
|
20
|
+
ChromaMemory,
|
|
21
|
+
ConversationMemory,
|
|
22
|
+
InMemory,
|
|
23
|
+
QdrantMemory,
|
|
24
|
+
ShortTermMemory,
|
|
25
|
+
)
|
|
26
|
+
from flowai.observability import EventBus, StreamEvent, Tracer
|
|
27
|
+
from flowai.providers import (
|
|
28
|
+
BaseProvider,
|
|
29
|
+
Message,
|
|
30
|
+
ProviderResponse,
|
|
31
|
+
StreamChunk,
|
|
32
|
+
ToolCall,
|
|
33
|
+
ToolResult,
|
|
34
|
+
)
|
|
35
|
+
from flowai.tools import BaseTool, ToolRegistry, tool
|
|
36
|
+
from flowai.workflow import END, START, AgentState, Workflow
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Agent",
|
|
40
|
+
"AssistantAgent",
|
|
41
|
+
"ToolAgent",
|
|
42
|
+
"PlannerAgent",
|
|
43
|
+
"CriticAgent",
|
|
44
|
+
"SupervisorAgent",
|
|
45
|
+
"ReflectionPattern",
|
|
46
|
+
"PlanAndExecutePattern",
|
|
47
|
+
"DebatePattern",
|
|
48
|
+
"Swarm",
|
|
49
|
+
"Workflow",
|
|
50
|
+
"AgentState",
|
|
51
|
+
"START",
|
|
52
|
+
"END",
|
|
53
|
+
"tool",
|
|
54
|
+
"BaseTool",
|
|
55
|
+
"ToolRegistry",
|
|
56
|
+
"Settings",
|
|
57
|
+
"BaseProvider",
|
|
58
|
+
"Message",
|
|
59
|
+
"ToolCall",
|
|
60
|
+
"ToolResult",
|
|
61
|
+
"ProviderResponse",
|
|
62
|
+
"StreamChunk",
|
|
63
|
+
"BaseMemory",
|
|
64
|
+
"InMemory",
|
|
65
|
+
"ShortTermMemory",
|
|
66
|
+
"ConversationMemory",
|
|
67
|
+
"QdrantMemory",
|
|
68
|
+
"ChromaMemory",
|
|
69
|
+
"Tracer",
|
|
70
|
+
"EventBus",
|
|
71
|
+
"StreamEvent",
|
|
72
|
+
]
|
flowai/__main__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""FlowAI Agent Subsystem."""
|
|
2
|
+
|
|
3
|
+
from flowai.agents.base import AgentContext, BaseAgent
|
|
4
|
+
from flowai.agents.patterns import DebatePattern, PlanAndExecutePattern, ReflectionPattern, Swarm
|
|
5
|
+
from flowai.agents.specialized import CriticAgent, PlannerAgent, SupervisorAgent, ToolAgent
|
|
6
|
+
from flowai.agents.standard import Agent, AssistantAgent
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BaseAgent",
|
|
10
|
+
"AgentContext",
|
|
11
|
+
"Agent",
|
|
12
|
+
"AssistantAgent",
|
|
13
|
+
"ToolAgent",
|
|
14
|
+
"PlannerAgent",
|
|
15
|
+
"CriticAgent",
|
|
16
|
+
"SupervisorAgent",
|
|
17
|
+
"ReflectionPattern",
|
|
18
|
+
"PlanAndExecutePattern",
|
|
19
|
+
"DebatePattern",
|
|
20
|
+
"Swarm",
|
|
21
|
+
]
|
flowai/agents/base.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Base Agent class and execution context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import AsyncIterator, Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from flowai.memory.base import BaseMemory
|
|
12
|
+
from flowai.observability.events import EventBus
|
|
13
|
+
from flowai.observability.tracer import Tracer
|
|
14
|
+
from flowai.providers.base import BaseProvider, StreamChunk
|
|
15
|
+
from flowai.providers.registry import get_provider
|
|
16
|
+
from flowai.tools.base import BaseTool
|
|
17
|
+
from flowai.tools.registry import ToolRegistry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentContext(BaseModel):
|
|
21
|
+
"""Context passed across agent turns."""
|
|
22
|
+
session_id: str
|
|
23
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseAgent(ABC):
|
|
27
|
+
"""Abstract Base Class for all agents in FlowAI."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
name: str = "Agent",
|
|
32
|
+
model: str = "gpt-4o",
|
|
33
|
+
provider: str | BaseProvider = "openai",
|
|
34
|
+
system_prompt: str = "You are a helpful autonomous AI assistant.",
|
|
35
|
+
tools: Sequence[BaseTool | Any] | ToolRegistry | None = None,
|
|
36
|
+
memory: BaseMemory | None = None,
|
|
37
|
+
max_iterations: int = 10,
|
|
38
|
+
temperature: float = 0.7,
|
|
39
|
+
tracer: Tracer | None = None,
|
|
40
|
+
event_bus: EventBus | None = None,
|
|
41
|
+
):
|
|
42
|
+
self.name = name
|
|
43
|
+
self.model = model
|
|
44
|
+
self.provider = get_provider(provider, model=model) if isinstance(provider, str) else provider
|
|
45
|
+
self.system_prompt = system_prompt
|
|
46
|
+
self.tools = ToolRegistry(tools) if not isinstance(tools, ToolRegistry) else tools
|
|
47
|
+
self.memory = memory
|
|
48
|
+
self.max_iterations = max_iterations
|
|
49
|
+
self.temperature = temperature
|
|
50
|
+
self.tracer = tracer or Tracer()
|
|
51
|
+
self.event_bus = event_bus or EventBus()
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def run(self, prompt: str, **kwargs: Any) -> str:
|
|
55
|
+
"""Execute agent reasoning on input prompt and return output."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def stream(self, prompt: str, **kwargs: Any) -> AsyncIterator[StreamChunk]:
|
|
60
|
+
"""Stream response tokens and event deltas."""
|
|
61
|
+
...
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Multi-agent execution patterns and topologies."""
|
|
2
|
+
|
|
3
|
+
from flowai.agents.patterns.debate import DebatePattern, Swarm
|
|
4
|
+
from flowai.agents.patterns.reflection import PlanAndExecutePattern, ReflectionPattern
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ReflectionPattern",
|
|
8
|
+
"PlanAndExecutePattern",
|
|
9
|
+
"DebatePattern",
|
|
10
|
+
"Swarm",
|
|
11
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Debate and Swarm multi-agent interaction topologies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from flowai.agents.standard import Agent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DebatePattern:
|
|
11
|
+
"""Multi-agent adversarial debate with a judge/moderator."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, debater_a: Agent, debater_b: Agent, judge: Agent, rounds: int = 2):
|
|
14
|
+
self.debater_a = debater_a
|
|
15
|
+
self.debater_b = debater_b
|
|
16
|
+
self.judge = judge
|
|
17
|
+
self.rounds = rounds
|
|
18
|
+
|
|
19
|
+
async def execute(self, topic: str) -> dict[str, Any]:
|
|
20
|
+
transcript = []
|
|
21
|
+
last_arg_a = await self.debater_a.run(f"Argue IN FAVOR of: {topic}")
|
|
22
|
+
transcript.append(f"{self.debater_a.name}: {last_arg_a}")
|
|
23
|
+
|
|
24
|
+
last_arg_b = await self.debater_b.run(f"Argue AGAINST: {topic}\nOpponent argument: {last_arg_a}")
|
|
25
|
+
transcript.append(f"{self.debater_b.name}: {last_arg_b}")
|
|
26
|
+
|
|
27
|
+
for r in range(1, self.rounds):
|
|
28
|
+
last_arg_a = await self.debater_a.run(f"Rebut: {last_arg_b}\nTopic: {topic}")
|
|
29
|
+
transcript.append(f"{self.debater_a.name} (Round {r+1}): {last_arg_a}")
|
|
30
|
+
|
|
31
|
+
last_arg_b = await self.debater_b.run(f"Rebut: {last_arg_a}\nTopic: {topic}")
|
|
32
|
+
transcript.append(f"{self.debater_b.name} (Round {r+1}): {last_arg_b}")
|
|
33
|
+
|
|
34
|
+
full_transcript = "\n\n".join(transcript)
|
|
35
|
+
verdict = await self.judge.run(
|
|
36
|
+
f"You are the impartial judge of this debate on '{topic}'. Review the transcript and determine the winner and reasoning:\n\n{full_transcript}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
"transcript": transcript,
|
|
41
|
+
"verdict": verdict,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Swarm:
|
|
46
|
+
"""Dynamic agent handoff network where agents can pass execution to peers."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, agents: list[Agent], initial_agent: Agent | None = None):
|
|
49
|
+
self.agents = {a.name: a for a in agents}
|
|
50
|
+
self.current_agent = initial_agent or agents[0]
|
|
51
|
+
|
|
52
|
+
def add_agent(self, agent: Agent) -> None:
|
|
53
|
+
self.agents[agent.name] = agent
|
|
54
|
+
|
|
55
|
+
async def execute(self, prompt: str, max_handoffs: int = 5) -> str:
|
|
56
|
+
current = self.current_agent
|
|
57
|
+
current_prompt = prompt
|
|
58
|
+
|
|
59
|
+
for _ in range(max_handoffs):
|
|
60
|
+
# inject peers into context
|
|
61
|
+
peer_names = [name for name in self.agents.keys() if name != current.name]
|
|
62
|
+
directive = (
|
|
63
|
+
f"\n[SYSTEM NOTICE]: If another agent is better suited, respond with 'HANDOFF_TO: <AgentName> <Instructions>'. "
|
|
64
|
+
f"Available agents: {', '.join(peer_names)}"
|
|
65
|
+
)
|
|
66
|
+
response = await current.run(current_prompt + directive)
|
|
67
|
+
|
|
68
|
+
if "HANDOFF_TO:" in response:
|
|
69
|
+
parts = response.split("HANDOFF_TO:", 1)[1].strip().split(" ", 1)
|
|
70
|
+
target_name = parts[0].strip()
|
|
71
|
+
handoff_instruction = parts[1].strip() if len(parts) > 1 else current_prompt
|
|
72
|
+
if target_name in self.agents:
|
|
73
|
+
current = self.agents[target_name]
|
|
74
|
+
current_prompt = handoff_instruction
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
return response
|
|
78
|
+
|
|
79
|
+
return f"Completed with agent {current.name}: {response}"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Reflection and Plan-and-Execute multi-agent patterns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from flowai.agents.specialized import CriticAgent, PlannerAgent
|
|
8
|
+
from flowai.agents.standard import Agent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReflectionPattern:
|
|
12
|
+
"""Actor-Critic reflection pattern: generate candidate -> critique -> refine."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, actor: Agent, critic: CriticAgent, max_rounds: int = 3):
|
|
15
|
+
self.actor = actor
|
|
16
|
+
self.critic = critic
|
|
17
|
+
self.max_rounds = max_rounds
|
|
18
|
+
|
|
19
|
+
async def execute(self, task: str) -> dict[str, Any]:
|
|
20
|
+
current_solution = await self.actor.run(f"Task: {task}\nProvide an initial solution:")
|
|
21
|
+
history = []
|
|
22
|
+
|
|
23
|
+
for round_idx in range(self.max_rounds):
|
|
24
|
+
evaluation = await self.critic.evaluate(task, current_solution)
|
|
25
|
+
history.append({
|
|
26
|
+
"round": round_idx + 1,
|
|
27
|
+
"solution": current_solution,
|
|
28
|
+
"critique": evaluation.get("critique"),
|
|
29
|
+
"is_approved": evaluation.get("is_approved"),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
if evaluation.get("is_approved"):
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
# Refine solution based on critique
|
|
36
|
+
refine_prompt = (
|
|
37
|
+
f"Your previous solution received this critique:\n{evaluation.get('critique')}\n\n"
|
|
38
|
+
f"Please produce an improved and corrected version of the solution for:\n{task}"
|
|
39
|
+
)
|
|
40
|
+
current_solution = await self.actor.run(refine_prompt)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"final_solution": current_solution,
|
|
44
|
+
"rounds_completed": len(history),
|
|
45
|
+
"history": history,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PlanAndExecutePattern:
|
|
50
|
+
"""Planner decomposes task, Worker executes each step sequentially."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, planner: PlannerAgent, worker: Agent):
|
|
53
|
+
self.planner = planner
|
|
54
|
+
self.worker = worker
|
|
55
|
+
|
|
56
|
+
async def execute(self, goal: str) -> dict[str, Any]:
|
|
57
|
+
plan = await self.planner.create_plan(goal)
|
|
58
|
+
step_results = []
|
|
59
|
+
cumulative_context = ""
|
|
60
|
+
|
|
61
|
+
for i, step in enumerate(plan, 1):
|
|
62
|
+
step_prompt = f"Goal: {goal}\nPrevious context:\n{cumulative_context}\nExecute Step {i}: {step}"
|
|
63
|
+
result = await self.worker.run(step_prompt)
|
|
64
|
+
step_results.append({"step": step, "output": result})
|
|
65
|
+
cumulative_context += f"\nStep {i} ({step}) output:\n{result}\n"
|
|
66
|
+
|
|
67
|
+
summary_prompt = f"Goal: {goal}\nReview all step outputs and provide a final answer:\n{cumulative_context}"
|
|
68
|
+
final_answer = await self.worker.run(summary_prompt)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"plan": plan,
|
|
72
|
+
"step_results": step_results,
|
|
73
|
+
"final_answer": final_answer,
|
|
74
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Specialized agents: ToolAgent, PlannerAgent, CriticAgent, SupervisorAgent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from flowai.agents.standard import Agent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ToolAgent(Agent):
|
|
12
|
+
"""Agent specialized strictly for executing tools and validating their results."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str = "ToolExpert", **kwargs):
|
|
15
|
+
system_prompt = kwargs.pop(
|
|
16
|
+
"system_prompt",
|
|
17
|
+
"You are a Tool Specialist. Always prioritize selecting the right tool, verifying arguments, and validating outputs.",
|
|
18
|
+
)
|
|
19
|
+
super().__init__(name=name, system_prompt=system_prompt, **kwargs)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PlannerAgent(Agent):
|
|
23
|
+
"""Agent that decomposes complex user queries into structured sequential steps."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, name: str = "Planner", **kwargs):
|
|
26
|
+
system_prompt = kwargs.pop(
|
|
27
|
+
"system_prompt",
|
|
28
|
+
"You are a Strategic Planning Agent. Analyze goals and output clear, numbered step-by-step execution plans.",
|
|
29
|
+
)
|
|
30
|
+
super().__init__(name=name, system_prompt=system_prompt, **kwargs)
|
|
31
|
+
|
|
32
|
+
async def create_plan(self, goal: str) -> list[str]:
|
|
33
|
+
prompt = f"Decompose the following goal into discrete execution steps. Output only the steps as a JSON array of strings:\nGoal: {goal}"
|
|
34
|
+
response = await self.run(prompt)
|
|
35
|
+
try:
|
|
36
|
+
start_idx = response.find("[")
|
|
37
|
+
end_idx = response.rfind("]") + 1
|
|
38
|
+
if start_idx != -1 and end_idx != -1:
|
|
39
|
+
return json.loads(response[start_idx:end_idx])
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
return [s.strip("- ") for s in response.split("\n") if s.strip()]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CriticAgent(Agent):
|
|
46
|
+
"""Agent that reviews, critiques, and suggests refinements to work products."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, name: str = "Critic", **kwargs):
|
|
49
|
+
system_prompt = kwargs.pop(
|
|
50
|
+
"system_prompt",
|
|
51
|
+
"You are a Critical Reviewer. Scrutinize logic, identify hallucinations, point out omissions, and propose fixes.",
|
|
52
|
+
)
|
|
53
|
+
super().__init__(name=name, system_prompt=system_prompt, **kwargs)
|
|
54
|
+
|
|
55
|
+
async def evaluate(self, task: str, solution: str) -> dict[str, Any]:
|
|
56
|
+
prompt = (
|
|
57
|
+
f"Evaluate the proposed solution for the given task.\n"
|
|
58
|
+
f"Task: {task}\n"
|
|
59
|
+
f"Solution: {solution}\n\n"
|
|
60
|
+
f"Respond with a JSON object containing: 'is_approved' (boolean), 'score' (1-10), 'critique' (string)."
|
|
61
|
+
)
|
|
62
|
+
res = await self.run(prompt)
|
|
63
|
+
try:
|
|
64
|
+
start_idx = res.find("{")
|
|
65
|
+
end_idx = res.rfind("}") + 1
|
|
66
|
+
if start_idx != -1 and end_idx != -1:
|
|
67
|
+
return json.loads(res[start_idx:end_idx])
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
return {"is_approved": True, "score": 8, "critique": res}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SupervisorAgent(Agent):
|
|
74
|
+
"""Agent that orchestrates worker agents and routes queries to the appropriate specialist."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, workers: dict[str, Agent] | None = None, name: str = "Supervisor", **kwargs):
|
|
77
|
+
self.workers = workers or {}
|
|
78
|
+
worker_descriptions = "\n".join([f"- {name}: {w.system_prompt}" for name, w in self.workers.items()])
|
|
79
|
+
system_prompt = kwargs.pop(
|
|
80
|
+
"system_prompt",
|
|
81
|
+
f"You are the Supervisor. Route questions to the appropriate worker agent:\n{worker_descriptions}\n"
|
|
82
|
+
f"Respond with the name of the worker agent followed by instructions.",
|
|
83
|
+
)
|
|
84
|
+
super().__init__(name=name, system_prompt=system_prompt, **kwargs)
|
|
85
|
+
|
|
86
|
+
def register_worker(self, name: str, agent: Agent) -> None:
|
|
87
|
+
self.workers[name] = agent
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Standard Agent and AssistantAgent implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from flowai.agents.base import BaseAgent
|
|
9
|
+
from flowai.providers.base import BaseProvider, Message, StreamChunk, ToolCall
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Agent(BaseAgent):
|
|
13
|
+
"""Core autonomous Agent capable of reasoning, tool execution, and memory recall."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, **kwargs: Any):
|
|
16
|
+
super().__init__(**kwargs)
|
|
17
|
+
self.messages: list[Message] = [
|
|
18
|
+
Message(role="system", content=self.system_prompt)
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------- helpers
|
|
22
|
+
|
|
23
|
+
def switch_model(self, model: str, provider: str | BaseProvider | None = None) -> None:
|
|
24
|
+
"""Switch provider or model mid-conversation while preserving full context history."""
|
|
25
|
+
from flowai.providers.registry import get_provider
|
|
26
|
+
self.model = model
|
|
27
|
+
if provider:
|
|
28
|
+
self.provider = get_provider(provider, model=model) if isinstance(provider, str) else provider
|
|
29
|
+
else:
|
|
30
|
+
# Re-instantiate current provider with new model
|
|
31
|
+
provider_name = self.provider.__class__.__name__.replace("Provider", "").lower()
|
|
32
|
+
self.provider = get_provider(provider_name, model=model, api_key=self.provider.api_key, base_url=self.provider.base_url)
|
|
33
|
+
|
|
34
|
+
def reset_session(self) -> None:
|
|
35
|
+
"""Clear conversation history back to initial system prompt."""
|
|
36
|
+
self.messages = [Message(role="system", content=self.system_prompt)]
|
|
37
|
+
|
|
38
|
+
def _tool_schemas(self) -> list[dict[str, Any]] | None:
|
|
39
|
+
if not self.tools or not self.tools.list_tools():
|
|
40
|
+
return None
|
|
41
|
+
return self.tools.get_schemas(format="openai")
|
|
42
|
+
|
|
43
|
+
async def _recall(self, prompt: str) -> None:
|
|
44
|
+
"""Pull relevant long-term memories into context, if a memory backend is attached."""
|
|
45
|
+
if not self.memory:
|
|
46
|
+
return
|
|
47
|
+
past = await self.memory.search(prompt, limit=5)
|
|
48
|
+
if past:
|
|
49
|
+
context_str = "\n".join(f"- [{p.role}] {p.content}" for p in past)
|
|
50
|
+
self.messages.append(
|
|
51
|
+
Message(role="system", content=f"Relevant background memories:\n{context_str}")
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
async def _run_tool_calls(self, tool_calls: list[ToolCall]) -> None:
|
|
55
|
+
"""Execute every requested tool call and append its result to the transcript."""
|
|
56
|
+
for tc in tool_calls:
|
|
57
|
+
await self.event_bus.publish("tool_start", {"tool": tc.name, "arguments": tc.arguments})
|
|
58
|
+
output = await self.tools.execute(tc.name, **tc.arguments)
|
|
59
|
+
output_str = output if isinstance(output, str) else str(output)
|
|
60
|
+
await self.event_bus.publish("tool_end", {"tool": tc.name, "result": output_str})
|
|
61
|
+
self.messages.append(
|
|
62
|
+
Message(role="tool", name=tc.name, tool_call_id=tc.id, content=output_str)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------- run
|
|
66
|
+
|
|
67
|
+
async def run(self, prompt: str, **kwargs: Any) -> str:
|
|
68
|
+
span = self.tracer.start_span(f"{self.name}.run", prompt=prompt, model=self.model)
|
|
69
|
+
await self.event_bus.publish("agent_start", {"agent": self.name, "prompt": prompt, "model": self.model})
|
|
70
|
+
|
|
71
|
+
await self._recall(prompt)
|
|
72
|
+
self.messages.append(Message(role="user", content=prompt))
|
|
73
|
+
|
|
74
|
+
final_reply = ""
|
|
75
|
+
hit_limit = True
|
|
76
|
+
for iteration in range(1, self.max_iterations + 1):
|
|
77
|
+
await self.event_bus.publish("agent_iteration", {"iteration": iteration, "agent": self.name})
|
|
78
|
+
|
|
79
|
+
response = await self.provider.generate(
|
|
80
|
+
messages=self.messages,
|
|
81
|
+
tools=self._tool_schemas(),
|
|
82
|
+
temperature=self.temperature,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.tracer.record_usage(
|
|
86
|
+
span,
|
|
87
|
+
self.model,
|
|
88
|
+
response.usage.prompt_tokens,
|
|
89
|
+
response.usage.completion_tokens,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# No tool calls means the model has reached its conclusion.
|
|
93
|
+
if not response.tool_calls:
|
|
94
|
+
final_reply = response.content
|
|
95
|
+
self.messages.append(Message(role="assistant", content=final_reply))
|
|
96
|
+
hit_limit = False
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
self.messages.append(
|
|
100
|
+
Message(role="assistant", content=response.content, tool_calls=response.tool_calls)
|
|
101
|
+
)
|
|
102
|
+
await self._run_tool_calls(response.tool_calls)
|
|
103
|
+
|
|
104
|
+
span.finish()
|
|
105
|
+
await self.event_bus.publish("agent_end", {"agent": self.name, "reply": final_reply})
|
|
106
|
+
|
|
107
|
+
if self.memory and final_reply:
|
|
108
|
+
await self.memory.add(f"User: {prompt}\nAssistant: {final_reply}")
|
|
109
|
+
|
|
110
|
+
if hit_limit and not final_reply:
|
|
111
|
+
return (
|
|
112
|
+
f"(Stopped after {self.max_iterations} tool-calling iterations without a final answer. "
|
|
113
|
+
"Raise max_iterations or simplify the task.)"
|
|
114
|
+
)
|
|
115
|
+
return final_reply or "(No output generated)"
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------- stream
|
|
118
|
+
|
|
119
|
+
async def stream(self, prompt: str, **kwargs: Any) -> AsyncIterator[StreamChunk]:
|
|
120
|
+
"""Stream the answer, running any tools the model asks for along the way.
|
|
121
|
+
|
|
122
|
+
Text deltas are yielded as they arrive. When the model asks for tools,
|
|
123
|
+
they run, their results are appended to the transcript, and the loop
|
|
124
|
+
continues streaming the next turn, so a streamed session is as capable
|
|
125
|
+
as a non-streamed one.
|
|
126
|
+
"""
|
|
127
|
+
span = self.tracer.start_span(f"{self.name}.stream", prompt=prompt, model=self.model)
|
|
128
|
+
await self.event_bus.publish("agent_start", {"agent": self.name, "prompt": prompt, "model": self.model})
|
|
129
|
+
|
|
130
|
+
await self._recall(prompt)
|
|
131
|
+
self.messages.append(Message(role="user", content=prompt))
|
|
132
|
+
|
|
133
|
+
for iteration in range(1, self.max_iterations + 1):
|
|
134
|
+
await self.event_bus.publish("agent_iteration", {"iteration": iteration, "agent": self.name})
|
|
135
|
+
|
|
136
|
+
text_parts: list[str] = []
|
|
137
|
+
tool_calls: list[ToolCall] = []
|
|
138
|
+
|
|
139
|
+
async for chunk in self.provider.stream(
|
|
140
|
+
messages=self.messages,
|
|
141
|
+
tools=self._tool_schemas(),
|
|
142
|
+
temperature=self.temperature,
|
|
143
|
+
):
|
|
144
|
+
if chunk.text_delta:
|
|
145
|
+
text_parts.append(chunk.text_delta)
|
|
146
|
+
await self.event_bus.publish("token", {"delta": chunk.text_delta})
|
|
147
|
+
if chunk.tool_call_delta is not None:
|
|
148
|
+
tool_calls.append(chunk.tool_call_delta)
|
|
149
|
+
if chunk.usage:
|
|
150
|
+
self.tracer.record_usage(
|
|
151
|
+
span, self.model, chunk.usage.prompt_tokens, chunk.usage.completion_tokens
|
|
152
|
+
)
|
|
153
|
+
yield chunk
|
|
154
|
+
|
|
155
|
+
reply_text = "".join(text_parts)
|
|
156
|
+
|
|
157
|
+
if not tool_calls:
|
|
158
|
+
if reply_text:
|
|
159
|
+
self.messages.append(Message(role="assistant", content=reply_text))
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
self.messages.append(
|
|
163
|
+
Message(role="assistant", content=reply_text, tool_calls=tool_calls)
|
|
164
|
+
)
|
|
165
|
+
await self._run_tool_calls(tool_calls)
|
|
166
|
+
|
|
167
|
+
span.finish()
|
|
168
|
+
await self.event_bus.publish("agent_end", {"agent": self.name, "reply": ""})
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class AssistantAgent(Agent):
|
|
172
|
+
"""Personalized assistant agent with customizable persona and role."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, name: str = "Assistant", persona: str = "Expert AI Consultant", **kwargs):
|
|
175
|
+
system_prompt = kwargs.pop("system_prompt", f"You are {name}, a {persona}. Provide rigorous, concise, and helpful responses.")
|
|
176
|
+
super().__init__(name=name, system_prompt=system_prompt, **kwargs)
|