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.
Files changed (56) hide show
  1. flowai/__init__.py +72 -0
  2. flowai/__main__.py +8 -0
  3. flowai/agents/__init__.py +21 -0
  4. flowai/agents/base.py +61 -0
  5. flowai/agents/patterns/__init__.py +11 -0
  6. flowai/agents/patterns/debate.py +79 -0
  7. flowai/agents/patterns/reflection.py +74 -0
  8. flowai/agents/specialized.py +87 -0
  9. flowai/agents/standard.py +176 -0
  10. flowai/cli/__init__.py +5 -0
  11. flowai/cli/main.py +562 -0
  12. flowai/cli/ui.py +98 -0
  13. flowai/config/__init__.py +5 -0
  14. flowai/config/settings.py +204 -0
  15. flowai/errors.py +30 -0
  16. flowai/mcp/__init__.py +5 -0
  17. flowai/mcp/client.py +77 -0
  18. flowai/memory/__init__.py +16 -0
  19. flowai/memory/base.py +45 -0
  20. flowai/memory/chroma.py +97 -0
  21. flowai/memory/in_memory.py +65 -0
  22. flowai/memory/qdrant.py +104 -0
  23. flowai/observability/__init__.py +15 -0
  24. flowai/observability/events.py +45 -0
  25. flowai/observability/opentelemetry.py +56 -0
  26. flowai/observability/tracer.py +80 -0
  27. flowai/plugins/__init__.py +5 -0
  28. flowai/plugins/loader.py +31 -0
  29. flowai/providers/__init__.py +35 -0
  30. flowai/providers/anthropic.py +268 -0
  31. flowai/providers/base.py +180 -0
  32. flowai/providers/gemini.py +230 -0
  33. flowai/providers/nvidia.py +57 -0
  34. flowai/providers/ollama.py +237 -0
  35. flowai/providers/openai.py +274 -0
  36. flowai/providers/registry.py +50 -0
  37. flowai/runtime/__init__.py +5 -0
  38. flowai/runtime/session.py +76 -0
  39. flowai/server/__init__.py +5 -0
  40. flowai/server/app.py +133 -0
  41. flowai/tools/__init__.py +39 -0
  42. flowai/tools/base.py +56 -0
  43. flowai/tools/builtin.py +128 -0
  44. flowai/tools/decorator.py +103 -0
  45. flowai/tools/files.py +197 -0
  46. flowai/tools/registry.py +84 -0
  47. flowai/tools/web.py +94 -0
  48. flowai/workflow/__init__.py +15 -0
  49. flowai/workflow/checkpoint.py +73 -0
  50. flowai/workflow/engine.py +144 -0
  51. flowai/workflow/state.py +43 -0
  52. flowai_cli-0.2.0.dist-info/METADATA +223 -0
  53. flowai_cli-0.2.0.dist-info/RECORD +56 -0
  54. flowai_cli-0.2.0.dist-info/WHEEL +4 -0
  55. flowai_cli-0.2.0.dist-info/entry_points.txt +2 -0
  56. 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,8 @@
1
+ """Allows `python -m flowai`."""
2
+
3
+ import sys
4
+
5
+ from flowai.cli.main import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -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)
flowai/cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """FlowAI CLI Package."""
2
+
3
+ from flowai.cli.main import main
4
+
5
+ __all__ = ["main"]