augagent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- augagent/__init__.py +90 -0
- augagent/agent.py +420 -0
- augagent/models.py +291 -0
- augagent/task.py +156 -0
- augagent/team.py +163 -0
- augagent/telemetry.py +50 -0
- augagent/tools.py +262 -0
- augagent-0.1.0.dist-info/METADATA +139 -0
- augagent-0.1.0.dist-info/RECORD +11 -0
- augagent-0.1.0.dist-info/WHEEL +5 -0
- augagent-0.1.0.dist-info/top_level.txt +1 -0
augagent/models.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Core Pydantic models for the augagent framework.
|
|
2
|
+
|
|
3
|
+
All data flowing through the system is strongly typed via Pydantic v2 models,
|
|
4
|
+
providing runtime validation, serialisation, and rich editor support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, SecretStr
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Enumerations
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
class Role(str, Enum):
|
|
23
|
+
"""Roles within a conversation turn."""
|
|
24
|
+
|
|
25
|
+
SYSTEM = "system"
|
|
26
|
+
USER = "user"
|
|
27
|
+
ASSISTANT = "assistant"
|
|
28
|
+
TOOL = "tool"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TaskStatus(str, Enum):
|
|
32
|
+
"""Lifecycle status of a :class:`Task`."""
|
|
33
|
+
|
|
34
|
+
PENDING = "pending"
|
|
35
|
+
IN_PROGRESS = "in_progress"
|
|
36
|
+
COMPLETED = "completed"
|
|
37
|
+
FAILED = "failed"
|
|
38
|
+
CANCELLED = "cancelled"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
# LLM CONFIGURATION SCHEMAS
|
|
43
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
45
|
+
class LLMConfig(BaseModel):
|
|
46
|
+
"""Declarative configuration for connecting to an LLM provider.
|
|
47
|
+
|
|
48
|
+
Supports any **OpenAI-compatible** API (OpenAI, Azure OpenAI, Ollama,
|
|
49
|
+
vLLM, LiteLLM, etc.) by pointing ``base_url`` at the right endpoint.
|
|
50
|
+
|
|
51
|
+
Example::
|
|
52
|
+
|
|
53
|
+
# Explicit key
|
|
54
|
+
config = LLMConfig(model="gpt-4o", api_key="sk-...")
|
|
55
|
+
|
|
56
|
+
# From environment (default: reads OPENAI_API_KEY)
|
|
57
|
+
config = LLMConfig(model="gpt-4o")
|
|
58
|
+
|
|
59
|
+
# Local Ollama
|
|
60
|
+
config = LLMConfig(
|
|
61
|
+
model="llama3",
|
|
62
|
+
base_url="http://localhost:11434/v1",
|
|
63
|
+
api_key="ollama",
|
|
64
|
+
)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
68
|
+
|
|
69
|
+
model: str = Field(
|
|
70
|
+
default="gpt-4o",
|
|
71
|
+
description="Model identifier (e.g. 'gpt-4o', 'claude-3-opus', 'llama3').",
|
|
72
|
+
)
|
|
73
|
+
api_key: SecretStr | None = Field(
|
|
74
|
+
default=None,
|
|
75
|
+
description=(
|
|
76
|
+
"API key for the provider. If not set, the framework reads the "
|
|
77
|
+
"environment variable specified by ``api_key_env_var``."
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
api_key_env_var: str = Field(
|
|
81
|
+
default="OPENAI_API_KEY",
|
|
82
|
+
description="Environment variable to read the API key from when ``api_key`` is None.",
|
|
83
|
+
)
|
|
84
|
+
base_url: str = Field(
|
|
85
|
+
default="https://api.openai.com/v1",
|
|
86
|
+
description="Base URL of the chat-completions API.",
|
|
87
|
+
)
|
|
88
|
+
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
|
|
89
|
+
max_tokens: int = Field(default=4096, ge=1)
|
|
90
|
+
top_p: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
91
|
+
frequency_penalty: float = Field(default=0.0, ge=-2.0, le=2.0)
|
|
92
|
+
presence_penalty: float = Field(default=0.0, ge=-2.0, le=2.0)
|
|
93
|
+
stop: list[str] | None = Field(
|
|
94
|
+
default=None,
|
|
95
|
+
description="Up to 4 sequences where the model will stop generating.",
|
|
96
|
+
)
|
|
97
|
+
timeout: float = Field(
|
|
98
|
+
default=120.0,
|
|
99
|
+
gt=0,
|
|
100
|
+
description="HTTP request timeout in seconds.",
|
|
101
|
+
)
|
|
102
|
+
max_retries: int = Field(
|
|
103
|
+
default=3,
|
|
104
|
+
ge=0,
|
|
105
|
+
description="Retries on transient HTTP errors (429, 500, 502, 503).",
|
|
106
|
+
)
|
|
107
|
+
extra_headers: dict[str, str] = Field(
|
|
108
|
+
default_factory=dict,
|
|
109
|
+
description="Additional HTTP headers sent with every request.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def resolve_api_key(self) -> str:
|
|
113
|
+
"""Return the API key, resolving from the environment if necessary.
|
|
114
|
+
|
|
115
|
+
Raises
|
|
116
|
+
------
|
|
117
|
+
ValueError
|
|
118
|
+
If no key is available from either the field or the env var.
|
|
119
|
+
"""
|
|
120
|
+
if self.api_key is not None:
|
|
121
|
+
return self.api_key.get_secret_value()
|
|
122
|
+
key = os.environ.get(self.api_key_env_var, "")
|
|
123
|
+
if not key:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"No API key provided and environment variable "
|
|
126
|
+
f"'{self.api_key_env_var}' is not set."
|
|
127
|
+
)
|
|
128
|
+
return key
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
132
|
+
# CHAT COMPLETION RESPONSE SCHEMAS (OpenAI-compatible)
|
|
133
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
134
|
+
|
|
135
|
+
class FunctionCall(BaseModel):
|
|
136
|
+
"""The function the model wants to invoke (name + raw JSON args)."""
|
|
137
|
+
|
|
138
|
+
name: str
|
|
139
|
+
arguments: str # JSON-encoded string — parsed by the caller
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ChatToolCall(BaseModel):
|
|
143
|
+
"""A single tool-call entry inside an assistant message."""
|
|
144
|
+
|
|
145
|
+
id: str
|
|
146
|
+
type: str = "function"
|
|
147
|
+
function: FunctionCall
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ChatMessage(BaseModel):
|
|
151
|
+
"""A message object as returned inside a ``Choice``."""
|
|
152
|
+
|
|
153
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
154
|
+
|
|
155
|
+
role: str
|
|
156
|
+
content: str | None = None
|
|
157
|
+
tool_calls: list[ChatToolCall] | None = None
|
|
158
|
+
refusal: str | None = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class Choice(BaseModel):
|
|
162
|
+
"""One candidate completion returned by the API."""
|
|
163
|
+
|
|
164
|
+
index: int = 0
|
|
165
|
+
message: ChatMessage
|
|
166
|
+
finish_reason: str | None = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TokenUsage(BaseModel):
|
|
170
|
+
"""Token-level usage statistics from the API response."""
|
|
171
|
+
|
|
172
|
+
prompt_tokens: int = 0
|
|
173
|
+
completion_tokens: int = 0
|
|
174
|
+
total_tokens: int = 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ChatCompletion(BaseModel):
|
|
178
|
+
"""Fully-parsed ``/chat/completions`` response.
|
|
179
|
+
|
|
180
|
+
Using a Pydantic model here means malformed API responses are caught
|
|
181
|
+
immediately with clear validation errors rather than producing silent
|
|
182
|
+
``KeyError`` s downstream.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
186
|
+
|
|
187
|
+
id: str = ""
|
|
188
|
+
object: str = "chat.completion"
|
|
189
|
+
created: int | None = None
|
|
190
|
+
model: str = ""
|
|
191
|
+
choices: list[Choice] = Field(default_factory=list)
|
|
192
|
+
usage: TokenUsage | None = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# Internal message models
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
class ToolCall(BaseModel):
|
|
200
|
+
"""Framework-internal representation of a tool invocation request."""
|
|
201
|
+
|
|
202
|
+
model_config = ConfigDict(frozen=True)
|
|
203
|
+
|
|
204
|
+
id: str = Field(default_factory=lambda: f"call_{uuid.uuid4().hex[:12]}")
|
|
205
|
+
tool_name: str = Field(..., description="Name of the tool to invoke.")
|
|
206
|
+
arguments: dict[str, Any] = Field(
|
|
207
|
+
default_factory=dict,
|
|
208
|
+
description="Keyword arguments for the tool.",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ToolResponse(BaseModel):
|
|
213
|
+
"""Encapsulates the result returned by a tool invocation."""
|
|
214
|
+
|
|
215
|
+
model_config = ConfigDict(frozen=True)
|
|
216
|
+
|
|
217
|
+
tool_call_id: str = Field(..., description="ID of the originating ToolCall.")
|
|
218
|
+
tool_name: str
|
|
219
|
+
content: str = Field(..., description="Serialised output from the tool.")
|
|
220
|
+
is_error: bool = False
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class Message(BaseModel):
|
|
224
|
+
"""A single message within an agent conversation.
|
|
225
|
+
|
|
226
|
+
Messages form the backbone of inter-agent and agent-LLM communication.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
model_config = ConfigDict(frozen=True)
|
|
230
|
+
|
|
231
|
+
id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
|
232
|
+
role: Role
|
|
233
|
+
content: str | None = None
|
|
234
|
+
tool_calls: list[ToolCall] = Field(default_factory=list)
|
|
235
|
+
tool_responses: list[ToolResponse] = Field(default_factory=list)
|
|
236
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
237
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# Agent configuration (serialisable subset of AugAgent fields)
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
class AgentConfig(BaseModel):
|
|
245
|
+
"""Declarative, serialisable configuration for an
|
|
246
|
+
:class:`~augagent.agent.AugAgent`.
|
|
247
|
+
|
|
248
|
+
Useful for persisting, templating, or sharing agent definitions
|
|
249
|
+
as JSON / YAML without carrying callable state.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
253
|
+
|
|
254
|
+
name: str = Field(..., min_length=1, max_length=128)
|
|
255
|
+
role: str = Field(..., description="A short descriptor of the agent's persona.")
|
|
256
|
+
goal: str = Field(..., description="What this agent is trying to achieve.")
|
|
257
|
+
backstory: str = Field(
|
|
258
|
+
default="",
|
|
259
|
+
description="Rich context that shapes the agent's behaviour.",
|
|
260
|
+
)
|
|
261
|
+
llm_config: LLMConfig = Field(default_factory=LLMConfig)
|
|
262
|
+
max_iterations: int = Field(
|
|
263
|
+
default=25,
|
|
264
|
+
ge=1,
|
|
265
|
+
description="Maximum ReAct loop iterations before the agent stops.",
|
|
266
|
+
)
|
|
267
|
+
allow_delegation: bool = Field(
|
|
268
|
+
default=False,
|
|
269
|
+
description="Whether this agent may delegate sub-tasks to teammates.",
|
|
270
|
+
)
|
|
271
|
+
verbose: bool = False
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
# Task result
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
class TaskResult(BaseModel):
|
|
279
|
+
"""The output produced when a :class:`~augagent.task.Task` completes."""
|
|
280
|
+
|
|
281
|
+
model_config = ConfigDict(frozen=True)
|
|
282
|
+
|
|
283
|
+
task_id: str
|
|
284
|
+
agent_name: str
|
|
285
|
+
status: TaskStatus = TaskStatus.COMPLETED
|
|
286
|
+
output: str = ""
|
|
287
|
+
raw_output: Any = None
|
|
288
|
+
token_usage: dict[str, int] = Field(default_factory=dict)
|
|
289
|
+
elapsed_seconds: float = 0.0
|
|
290
|
+
iterations: int = 0
|
|
291
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
augagent/task.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Task — a discrete unit of work assigned to an AugAgent.
|
|
2
|
+
|
|
3
|
+
Tasks describe *what* needs to be done, *what* the expected output looks
|
|
4
|
+
like, and *which* agent is responsible. They are the currency of work
|
|
5
|
+
within an :class:`~augagent.team.AugTeam`.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
from augagent import AugAgent, AugTask, LLMConfig
|
|
10
|
+
|
|
11
|
+
writer = AugAgent(
|
|
12
|
+
name="Writer",
|
|
13
|
+
role="Content Writer",
|
|
14
|
+
goal="Write great copy",
|
|
15
|
+
llm_config=LLMConfig(model="gpt-4o"),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
task = AugTask(
|
|
19
|
+
description="Write a blog post about AI agents",
|
|
20
|
+
expected_output="A 500-word blog post in Markdown format",
|
|
21
|
+
agent=writer,
|
|
22
|
+
)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import uuid
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
31
|
+
|
|
32
|
+
from augagent.agent import AugAgent
|
|
33
|
+
from augagent.models import TaskResult, TaskStatus
|
|
34
|
+
from augagent.telemetry import get_logger
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AugTask(BaseModel):
|
|
38
|
+
"""A unit of work to be executed by an :class:`AugAgent`.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
description:
|
|
43
|
+
A natural-language description of what needs to be done.
|
|
44
|
+
expected_output:
|
|
45
|
+
A description of the ideal output format and content.
|
|
46
|
+
agent:
|
|
47
|
+
The agent responsible for executing this task.
|
|
48
|
+
context:
|
|
49
|
+
Optional list of predecessor :class:`AugTask` instances whose outputs
|
|
50
|
+
are prepended to this task's prompt, enabling information flow
|
|
51
|
+
across sequential tasks.
|
|
52
|
+
async_execution:
|
|
53
|
+
If ``True``, the task is eligible for concurrent execution.
|
|
54
|
+
output_json:
|
|
55
|
+
Optional Pydantic model class to validate and parse the output.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
59
|
+
|
|
60
|
+
id: str = Field(default_factory=lambda: uuid.uuid4().hex[:12])
|
|
61
|
+
description: str = Field(..., min_length=1)
|
|
62
|
+
expected_output: str = Field(default="")
|
|
63
|
+
agent: AugAgent | None = None
|
|
64
|
+
context: list["AugTask"] = Field(default_factory=list)
|
|
65
|
+
async_execution: bool = False
|
|
66
|
+
output_json: Any = Field(default=None, exclude=True, repr=False)
|
|
67
|
+
|
|
68
|
+
# -- runtime state (excluded from serialisation) ------------------------
|
|
69
|
+
status: TaskStatus = Field(default=TaskStatus.PENDING, exclude=True)
|
|
70
|
+
result: TaskResult | None = Field(default=None, exclude=True)
|
|
71
|
+
|
|
72
|
+
# -- public API ---------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
async def execute(self, agent: AugAgent | None = None) -> TaskResult:
|
|
75
|
+
"""Run the task using its assigned agent (or an override).
|
|
76
|
+
|
|
77
|
+
Raises
|
|
78
|
+
------
|
|
79
|
+
ValueError
|
|
80
|
+
If no agent is assigned or provided.
|
|
81
|
+
"""
|
|
82
|
+
executor = agent or self.agent
|
|
83
|
+
if executor is None:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"Task '{self.id}' has no assigned agent. "
|
|
86
|
+
"Pass an agent explicitly or set task.agent."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
logger = get_logger()
|
|
90
|
+
self.status = TaskStatus.IN_PROGRESS
|
|
91
|
+
|
|
92
|
+
# Build the full prompt, incorporating context from upstream tasks
|
|
93
|
+
prompt = self._build_prompt()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
result = await executor.execute(prompt)
|
|
97
|
+
# Re-stamp with this task's id
|
|
98
|
+
result = result.model_copy(update={"task_id": self.id})
|
|
99
|
+
self.result = result
|
|
100
|
+
self.status = TaskStatus.COMPLETED
|
|
101
|
+
|
|
102
|
+
# Optional JSON output validation
|
|
103
|
+
if self.output_json is not None:
|
|
104
|
+
try:
|
|
105
|
+
self.output_json.model_validate_json(result.output)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
logger.log_error(f"JSON validation failed: {exc}")
|
|
108
|
+
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
self.status = TaskStatus.FAILED
|
|
113
|
+
logger.log_error(f"Task failed: {exc}")
|
|
114
|
+
return TaskResult(
|
|
115
|
+
task_id=self.id,
|
|
116
|
+
agent_name=executor.name,
|
|
117
|
+
status=TaskStatus.FAILED,
|
|
118
|
+
output=f"Task failed: {exc}",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# -- internals ----------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def _build_prompt(self) -> str:
|
|
124
|
+
"""Compose the task prompt, prepending upstream context if present."""
|
|
125
|
+
parts: list[str] = []
|
|
126
|
+
|
|
127
|
+
if self.context:
|
|
128
|
+
parts.append("## Context from previous tasks\n")
|
|
129
|
+
for ctx_task in self.context:
|
|
130
|
+
if ctx_task.result and ctx_task.result.output:
|
|
131
|
+
parts.append(
|
|
132
|
+
f"### Output from '{ctx_task.description}'\n{ctx_task.result.output}\n"
|
|
133
|
+
)
|
|
134
|
+
parts.append("---\n")
|
|
135
|
+
|
|
136
|
+
parts.append(f"## Task\n{self.description}\n")
|
|
137
|
+
if self.expected_output:
|
|
138
|
+
parts.append(f"## Expected Output\n{self.expected_output}\n")
|
|
139
|
+
|
|
140
|
+
return "\n".join(parts)
|
|
141
|
+
|
|
142
|
+
def interpolate_inputs(self, inputs: dict[str, Any]) -> None:
|
|
143
|
+
"""Interpolate inputs into description and expected_output."""
|
|
144
|
+
self.description = self.description.format(**inputs)
|
|
145
|
+
if self.expected_output:
|
|
146
|
+
self.expected_output = self.expected_output.format(**inputs)
|
|
147
|
+
|
|
148
|
+
def __repr__(self) -> str:
|
|
149
|
+
agent_name = self.agent.name if self.agent else "unassigned"
|
|
150
|
+
return (
|
|
151
|
+
f"AugTask(id={self.id!r}, agent={agent_name!r}, "
|
|
152
|
+
f"status={self.status.value!r})"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Backward compatibility alias
|
|
156
|
+
Task = AugTask
|
augagent/team.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Team — multi-agent orchestration.
|
|
2
|
+
|
|
3
|
+
An :class:`AugTeam` groups agents and tasks together. In sequential mode,
|
|
4
|
+
it executes tasks one by one, automatically chaining the output of one task
|
|
5
|
+
as context for the next.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
from augagent import AugAgent, AugTask, AugTeam, LLMConfig
|
|
10
|
+
|
|
11
|
+
config = LLMConfig(model="gpt-4o")
|
|
12
|
+
researcher = AugAgent(name="Researcher", role="Researcher", goal="Find facts", llm_config=config)
|
|
13
|
+
writer = AugAgent(name="Writer", role="Writer", goal="Write articles", llm_config=config)
|
|
14
|
+
|
|
15
|
+
t1 = AugTask(description="Research AI trends", expected_output="Bullet points", agent=researcher)
|
|
16
|
+
t2 = AugTask(description="Write blog post", expected_output="Markdown article", agent=writer)
|
|
17
|
+
|
|
18
|
+
# Output from t1 is automatically passed as context to t2 in sequential mode
|
|
19
|
+
team = AugTeam(
|
|
20
|
+
agents=[researcher, writer],
|
|
21
|
+
tasks=[t1, t2],
|
|
22
|
+
)
|
|
23
|
+
results = team.kickoff()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
import time
|
|
30
|
+
from enum import Enum
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
34
|
+
|
|
35
|
+
from augagent.agent import AugAgent
|
|
36
|
+
from augagent.models import TaskResult, TaskStatus
|
|
37
|
+
from augagent.task import AugTask
|
|
38
|
+
from augagent.telemetry import get_logger
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Process(str, Enum):
|
|
42
|
+
"""Orchestration strategy for a :class:`AugTeam`."""
|
|
43
|
+
|
|
44
|
+
SEQUENTIAL = "sequential"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AugTeam(BaseModel):
|
|
48
|
+
"""A coordinated group of :class:`AugAgent` instances executing :class:`AugTask` objects.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
agents:
|
|
53
|
+
The roster of agents available to this team.
|
|
54
|
+
tasks:
|
|
55
|
+
An ordered list of tasks to execute.
|
|
56
|
+
process:
|
|
57
|
+
The orchestration strategy (currently only ``"sequential"``).
|
|
58
|
+
verbose:
|
|
59
|
+
Enable detailed logging.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
63
|
+
|
|
64
|
+
agents: list[AugAgent] = Field(..., min_length=1)
|
|
65
|
+
tasks: list[AugTask] = Field(..., min_length=1)
|
|
66
|
+
process: Process = Process.SEQUENTIAL
|
|
67
|
+
verbose: bool = False
|
|
68
|
+
|
|
69
|
+
# -- public API ---------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def kickoff(self, inputs: dict[str, Any] | None = None) -> list[TaskResult]:
|
|
72
|
+
"""Execute all tasks sequentially and return results.
|
|
73
|
+
|
|
74
|
+
Passes the output of each completed task as context to the next task.
|
|
75
|
+
Handles both sync and async contexts transparently.
|
|
76
|
+
"""
|
|
77
|
+
if inputs:
|
|
78
|
+
for task in self.tasks:
|
|
79
|
+
task.interpolate_inputs(inputs)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
loop = asyncio.get_running_loop()
|
|
83
|
+
except RuntimeError:
|
|
84
|
+
loop = None
|
|
85
|
+
|
|
86
|
+
if loop and loop.is_running():
|
|
87
|
+
import concurrent.futures
|
|
88
|
+
|
|
89
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
90
|
+
return pool.submit(asyncio.run, self._run()).result()
|
|
91
|
+
return asyncio.run(self._run())
|
|
92
|
+
|
|
93
|
+
async def akickoff(self, inputs: dict[str, Any] | None = None) -> list[TaskResult]:
|
|
94
|
+
"""Async version of :meth:`kickoff`."""
|
|
95
|
+
if inputs:
|
|
96
|
+
for task in self.tasks:
|
|
97
|
+
task.interpolate_inputs(inputs)
|
|
98
|
+
return await self._run()
|
|
99
|
+
|
|
100
|
+
# -- orchestration strategies -------------------------------------------
|
|
101
|
+
|
|
102
|
+
async def _run(self) -> list[TaskResult]:
|
|
103
|
+
logger = get_logger()
|
|
104
|
+
start = time.time()
|
|
105
|
+
|
|
106
|
+
if self.verbose:
|
|
107
|
+
logger.log_info(f"Team kickoff started with {len(self.tasks)} tasks.")
|
|
108
|
+
|
|
109
|
+
results = await self._run_sequential(logger)
|
|
110
|
+
|
|
111
|
+
elapsed = time.time() - start
|
|
112
|
+
completed = sum(1 for r in results if r.status == TaskStatus.COMPLETED)
|
|
113
|
+
failed = sum(1 for r in results if r.status == TaskStatus.FAILED)
|
|
114
|
+
|
|
115
|
+
if self.verbose:
|
|
116
|
+
logger.log_info(f"Team finished in {elapsed:.2f}s ({completed} completed, {failed} failed).")
|
|
117
|
+
|
|
118
|
+
return results
|
|
119
|
+
|
|
120
|
+
async def _run_sequential(self, logger: Any) -> list[TaskResult]:
|
|
121
|
+
"""Execute tasks one-by-one, passing outputs as context to the next."""
|
|
122
|
+
results: list[TaskResult] = []
|
|
123
|
+
|
|
124
|
+
for i, task in enumerate(self.tasks):
|
|
125
|
+
# Automatically chain context: if this isn't the first task, and the previous task
|
|
126
|
+
# successfully generated output, append it to this task's context.
|
|
127
|
+
if i > 0 and results[-1].status == TaskStatus.COMPLETED:
|
|
128
|
+
prev_task = self.tasks[i - 1]
|
|
129
|
+
if prev_task not in task.context:
|
|
130
|
+
task.context.append(prev_task)
|
|
131
|
+
|
|
132
|
+
executor = task.agent or self.agents[0]
|
|
133
|
+
|
|
134
|
+
logger.log_handoff(from_agent="Manager", to_agent=executor.name, task_desc=task.description)
|
|
135
|
+
|
|
136
|
+
result = await task.execute(agent=executor)
|
|
137
|
+
results.append(result)
|
|
138
|
+
|
|
139
|
+
if result.status == TaskStatus.COMPLETED:
|
|
140
|
+
logger.log_info(f"Task completed successfully. Output length: {len(result.output)} chars.")
|
|
141
|
+
else:
|
|
142
|
+
logger.log_error(f"Task failed: {result.output}")
|
|
143
|
+
|
|
144
|
+
return results
|
|
145
|
+
|
|
146
|
+
# -- utilities ----------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def get_agent(self, name: str) -> AugAgent | None:
|
|
149
|
+
"""Look up an agent by name."""
|
|
150
|
+
for agent in self.agents:
|
|
151
|
+
if agent.name == name:
|
|
152
|
+
return agent
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
def __repr__(self) -> str:
|
|
156
|
+
return (
|
|
157
|
+
f"AugTeam(agents={[a.name for a in self.agents]}, "
|
|
158
|
+
f"tasks={len(self.tasks)}, process={self.process.value!r})"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# Backward compatibility alias
|
|
163
|
+
Team = AugTeam
|
augagent/telemetry.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Telemetry & logging primitives.
|
|
2
|
+
|
|
3
|
+
Provides a lightweight logger that prints agent handoffs and tool executions
|
|
4
|
+
to the console in a clean, readable format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
# Configure a basic console logger
|
|
13
|
+
_logger = logging.getLogger("augagent")
|
|
14
|
+
_logger.setLevel(logging.INFO)
|
|
15
|
+
|
|
16
|
+
if not _logger.handlers:
|
|
17
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
18
|
+
formatter = logging.Formatter("%(message)s")
|
|
19
|
+
handler.setFormatter(formatter)
|
|
20
|
+
_logger.addHandler(handler)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AgentLogger:
|
|
24
|
+
"""A lightweight logger for tracking agent orchestration."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, logger: logging.Logger = _logger):
|
|
27
|
+
self._logger = logger
|
|
28
|
+
|
|
29
|
+
def log_handoff(self, from_agent: str, to_agent: str, task_desc: str) -> None:
|
|
30
|
+
"""Log a task handoff to an agent."""
|
|
31
|
+
self._logger.info(f"\n[{from_agent} -> {to_agent}] Starting Task: {task_desc}")
|
|
32
|
+
|
|
33
|
+
def log_tool_execution(self, agent_name: str, tool_name: str, args: dict[str, Any] | str) -> None:
|
|
34
|
+
"""Log when an agent executes a tool."""
|
|
35
|
+
self._logger.info(f" [Tool Execution] {agent_name} is using '{tool_name}' with args: {args}")
|
|
36
|
+
|
|
37
|
+
def log_info(self, message: str) -> None:
|
|
38
|
+
"""Log a general info message."""
|
|
39
|
+
self._logger.info(f" [Info] {message}")
|
|
40
|
+
|
|
41
|
+
def log_error(self, message: str) -> None:
|
|
42
|
+
"""Log an error message."""
|
|
43
|
+
self._logger.error(f" [Error] {message}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_DEFAULT_LOGGER = AgentLogger()
|
|
47
|
+
|
|
48
|
+
def get_logger() -> AgentLogger:
|
|
49
|
+
"""Return the global agent logger instance."""
|
|
50
|
+
return _DEFAULT_LOGGER
|