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/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