agentropic 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentropic-0.2.0 → agentropic-0.2.2}/PKG-INFO +1 -1
- {agentropic-0.2.0 → agentropic-0.2.2}/examples/basic_agent.py +1 -1
- {agentropic-0.2.0 → agentropic-0.2.2}/pyproject.toml +1 -1
- {agentropic-0.2.0 → agentropic-0.2.2}/src/agentropic/__init__.py +3 -0
- {agentropic-0.2.0 → agentropic-0.2.2}/src/agentropic/agent.py +38 -2
- agentropic-0.2.2/src/agentropic/tracing.py +269 -0
- {agentropic-0.2.0 → agentropic-0.2.2}/.gitignore +0 -0
- {agentropic-0.2.0 → agentropic-0.2.2}/LICENSE +0 -0
- {agentropic-0.2.0 → agentropic-0.2.2}/README.md +0 -0
- {agentropic-0.2.0 → agentropic-0.2.2}/src/agentropic/llm.py +0 -0
- {agentropic-0.2.0 → agentropic-0.2.2}/src/agentropic/mcp/__init__.py +0 -0
- {agentropic-0.2.0 → agentropic-0.2.2}/src/agentropic/mcp/client.py +0 -0
- {agentropic-0.2.0 → agentropic-0.2.2}/src/agentropic/memory.py +0 -0
- {agentropic-0.2.0 → agentropic-0.2.2}/src/agentropic/tools.py +0 -0
- {agentropic-0.2.0 → agentropic-0.2.2}/tests/test_agent.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentropic
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A minimal, hackable agentic framework: LLM calls via litellm, @tool-decorated functions, and MCP server support.
|
|
5
5
|
Author-email: Vishal Tiwari <vishaltiwari.up2019@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -31,7 +31,7 @@ async def main():
|
|
|
31
31
|
tools=[get_weather, add],
|
|
32
32
|
# Optional: pull in tools from a live MCP server too.
|
|
33
33
|
# mcp_servers=[StdioServer(command="npx", args=["-y", "@modelcontextprotocol/server-filesystem", "."])],
|
|
34
|
-
max_iterations=
|
|
34
|
+
max_iterations=2,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
result = await agent.arun("What's the weather in Tokyo, and what's 12 + 30?")
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentropic"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "A minimal, hackable agentic framework: LLM calls via litellm, @tool-decorated functions, and MCP server support."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -3,6 +3,7 @@ from .tools import tool, Tool, ToolRegistry
|
|
|
3
3
|
from .llm import LLMClient, LLMResponse
|
|
4
4
|
from .memory import Memory
|
|
5
5
|
from .mcp.client import MCPConnection, StdioServer, SSEServer, MCPServerConfig
|
|
6
|
+
from .tracing import AgentTracer, NullTracer
|
|
6
7
|
|
|
7
8
|
__version__ = "0.2.0"
|
|
8
9
|
|
|
@@ -21,4 +22,6 @@ __all__ = [
|
|
|
21
22
|
"StdioServer",
|
|
22
23
|
"SSEServer",
|
|
23
24
|
"MCPServerConfig",
|
|
25
|
+
"AgentTracer",
|
|
26
|
+
"NullTracer",
|
|
24
27
|
]
|
|
@@ -3,6 +3,11 @@ optional MCP servers into a single run()/arun() loop that:
|
|
|
3
3
|
1. sends messages + tool schemas to the LLM
|
|
4
4
|
2. if the LLM asks for tool calls, executes them and feeds results back
|
|
5
5
|
3. repeats until the LLM returns a final answer or max_iterations hits
|
|
6
|
+
|
|
7
|
+
Set `verbose=True` (default) to get CrewAI-style colored terminal traces
|
|
8
|
+
of every step — LLM calls, tool calls/results, final answer — via the
|
|
9
|
+
zero-dependency `tracing.AgentTracer`. Pass `verbose=False` to silence it,
|
|
10
|
+
or `tracer=YourTracer(...)` to plug in your own sink.
|
|
6
11
|
"""
|
|
7
12
|
from __future__ import annotations
|
|
8
13
|
|
|
@@ -15,6 +20,7 @@ from .llm import LLMClient, LLMResponse
|
|
|
15
20
|
from .memory import Memory
|
|
16
21
|
from .tools import Tool, ToolRegistry
|
|
17
22
|
from .mcp.client import MCPConnection, MCPServerConfig
|
|
23
|
+
from .tracing import AgentTracer, NullTracer
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
class AgentError(Exception):
|
|
@@ -120,6 +126,8 @@ class Agent:
|
|
|
120
126
|
api_key: t.Optional[str] = None,
|
|
121
127
|
api_base: t.Optional[str] = None,
|
|
122
128
|
temperature: float = 0.7,
|
|
129
|
+
verbose: bool = True,
|
|
130
|
+
tracer: t.Optional[AgentTracer] = None,
|
|
123
131
|
**llm_kwargs,
|
|
124
132
|
):
|
|
125
133
|
self.name = name
|
|
@@ -137,6 +145,12 @@ class Agent:
|
|
|
137
145
|
)
|
|
138
146
|
self._mcp_tools_loaded = False
|
|
139
147
|
|
|
148
|
+
# -- tracing / pretty terminal logs ----------------------------
|
|
149
|
+
# `tracer` wins if explicitly supplied; otherwise `verbose` picks
|
|
150
|
+
# between the real colorful tracer and a silent no-op one.
|
|
151
|
+
self.verbose = verbose
|
|
152
|
+
self.tracer = tracer or (AgentTracer(name) if verbose else NullTracer(name))
|
|
153
|
+
|
|
140
154
|
# -- tool management -------------------------------------------------
|
|
141
155
|
|
|
142
156
|
def add_tool(self, item: t.Union[Tool, t.Callable]) -> Tool:
|
|
@@ -170,6 +184,8 @@ class Agent:
|
|
|
170
184
|
error = False
|
|
171
185
|
started = time.monotonic()
|
|
172
186
|
|
|
187
|
+
self.tracer.tool_call_start(name, args)
|
|
188
|
+
|
|
173
189
|
if tl is None:
|
|
174
190
|
result = f"Error: tool '{name}' is not registered."
|
|
175
191
|
error = True
|
|
@@ -181,6 +197,7 @@ class Agent:
|
|
|
181
197
|
error = True
|
|
182
198
|
|
|
183
199
|
duration = time.monotonic() - started
|
|
200
|
+
self.tracer.tool_call_end(name, str(result), duration, error)
|
|
184
201
|
|
|
185
202
|
tool_msg = {
|
|
186
203
|
"role": "tool",
|
|
@@ -208,6 +225,8 @@ class Agent:
|
|
|
208
225
|
|
|
209
226
|
async def arun(self, user_input: str) -> AgentRunResult:
|
|
210
227
|
run_started = time.monotonic()
|
|
228
|
+
self.tracer.run_start(user_input)
|
|
229
|
+
|
|
211
230
|
await self._load_mcp_tools()
|
|
212
231
|
|
|
213
232
|
messages = self._build_messages(user_input)
|
|
@@ -219,9 +238,18 @@ class Agent:
|
|
|
219
238
|
raw_responses: list[LLMResponse] = []
|
|
220
239
|
|
|
221
240
|
for iteration in range(1, self.max_iterations + 1):
|
|
241
|
+
self.tracer.iteration_start(iteration, self.max_iterations)
|
|
242
|
+
llm_started = time.monotonic()
|
|
222
243
|
response = await self.llm.acomplete(messages, tools=tools_schema)
|
|
244
|
+
llm_duration = time.monotonic() - llm_started
|
|
223
245
|
raw_responses.append(response)
|
|
224
246
|
|
|
247
|
+
self.tracer.llm_response(
|
|
248
|
+
response.content or "",
|
|
249
|
+
[tc.function.name for tc in (response.tool_calls or [])],
|
|
250
|
+
llm_duration,
|
|
251
|
+
)
|
|
252
|
+
|
|
225
253
|
if response.tool_calls:
|
|
226
254
|
assistant_msg = {
|
|
227
255
|
"role": "assistant",
|
|
@@ -242,7 +270,7 @@ class Agent:
|
|
|
242
270
|
|
|
243
271
|
# No tool calls -> final answer
|
|
244
272
|
self.memory.add("assistant", response.content or "")
|
|
245
|
-
|
|
273
|
+
result = AgentRunResult(
|
|
246
274
|
output=response.content or "",
|
|
247
275
|
messages=messages,
|
|
248
276
|
tool_calls=all_tool_calls,
|
|
@@ -252,6 +280,11 @@ class Agent:
|
|
|
252
280
|
stopped_on_max_iterations=False,
|
|
253
281
|
duration_seconds=time.monotonic() - run_started,
|
|
254
282
|
)
|
|
283
|
+
self.tracer.final_answer(
|
|
284
|
+
result.output, result.iterations, len(result.tool_calls),
|
|
285
|
+
result.duration_seconds, result.usage,
|
|
286
|
+
)
|
|
287
|
+
return result
|
|
255
288
|
|
|
256
289
|
# Hit max_iterations without a final answer. Return what we have
|
|
257
290
|
# instead of only raising, so the caller can inspect what happened —
|
|
@@ -267,6 +300,7 @@ class Agent:
|
|
|
267
300
|
stopped_on_max_iterations=True,
|
|
268
301
|
duration_seconds=time.monotonic() - run_started,
|
|
269
302
|
)
|
|
303
|
+
self.tracer.max_iterations_error(self.max_iterations)
|
|
270
304
|
error = AgentError(
|
|
271
305
|
f"Agent '{self.name}' hit max_iterations ({self.max_iterations}) without a final answer."
|
|
272
306
|
)
|
|
@@ -288,10 +322,12 @@ class Agent:
|
|
|
288
322
|
yield result.output
|
|
289
323
|
return
|
|
290
324
|
|
|
325
|
+
self.tracer.run_start(user_input)
|
|
291
326
|
messages = self._build_messages(user_input)
|
|
292
327
|
self.memory.add("user", user_input)
|
|
293
328
|
full = ""
|
|
294
329
|
async for chunk in self.llm.astream(messages):
|
|
295
330
|
full += chunk
|
|
296
331
|
yield chunk
|
|
297
|
-
self.memory.add("assistant", full)
|
|
332
|
+
self.memory.add("assistant", full)
|
|
333
|
+
self.tracer.final_answer(full, 1, 0, 0.0, {})
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Colorful terminal tracing for Agent.run()/arun(), CrewAI-style.
|
|
2
|
+
|
|
3
|
+
Zero dependencies (pure ANSI escape codes) so it works in any terminal
|
|
4
|
+
without needing `rich` or similar installed. Wraps every step of the
|
|
5
|
+
agent loop — LLM calls, tool calls, tool results, final answer — in
|
|
6
|
+
boxed, colored, timestamped output.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
agent = Agent(..., verbose=True) # on by default
|
|
10
|
+
agent = Agent(..., verbose=False) # silence it
|
|
11
|
+
agent = Agent(..., tracer=MyTracer(...)) # bring your own sink
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import shutil
|
|
16
|
+
import sys
|
|
17
|
+
import textwrap
|
|
18
|
+
import time
|
|
19
|
+
import typing as t
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# --------------------------------------------------------------------------
|
|
23
|
+
# Low-level ANSI helpers
|
|
24
|
+
# --------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
class C:
|
|
27
|
+
RESET = "\033[0m"
|
|
28
|
+
BOLD = "\033[1m"
|
|
29
|
+
DIM = "\033[2m"
|
|
30
|
+
ITALIC = "\033[3m"
|
|
31
|
+
|
|
32
|
+
CYAN = "\033[36m"
|
|
33
|
+
MAGENTA = "\033[35m"
|
|
34
|
+
YELLOW = "\033[33m"
|
|
35
|
+
GREEN = "\033[32m"
|
|
36
|
+
RED = "\033[31m"
|
|
37
|
+
BLUE = "\033[34m"
|
|
38
|
+
WHITE = "\033[97m"
|
|
39
|
+
GREY = "\033[90m"
|
|
40
|
+
|
|
41
|
+
BG_CYAN = "\033[46m"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _supports_color() -> bool:
|
|
45
|
+
"""Disable colors when piped/redirected (mirrors how most CLI tools behave)."""
|
|
46
|
+
return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _term_width(default: int = 88) -> int:
|
|
50
|
+
try:
|
|
51
|
+
return min(shutil.get_terminal_size().columns, 120)
|
|
52
|
+
except Exception:
|
|
53
|
+
return default
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _wrap(text: str, width: int, indent: str = "") -> list[str]:
|
|
57
|
+
out: list[str] = []
|
|
58
|
+
for line in (text or "").splitlines() or [""]:
|
|
59
|
+
wrapped = textwrap.wrap(
|
|
60
|
+
line, width=width, subsequent_indent=indent, initial_indent=indent
|
|
61
|
+
) or [indent]
|
|
62
|
+
out.extend(wrapped)
|
|
63
|
+
return out
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _truncate(text: str, n: int) -> str:
|
|
67
|
+
text = text or ""
|
|
68
|
+
return text if len(text) <= n else text[: n - 1].rstrip() + "…"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _display_width(text: str) -> int:
|
|
72
|
+
"""Approximate terminal column width, counting emoji/wide symbols as 2
|
|
73
|
+
columns (most terminals render them that way even though len() sees 1
|
|
74
|
+
codepoint). Good enough for box alignment without a wcwidth dependency."""
|
|
75
|
+
width = 0
|
|
76
|
+
for ch in text:
|
|
77
|
+
cp = ord(ch)
|
|
78
|
+
if (
|
|
79
|
+
0x1F300 <= cp <= 0x1FAFF # emoji blocks
|
|
80
|
+
or 0x2600 <= cp <= 0x27BF # misc symbols / dingbats (✅ ⚠ etc.)
|
|
81
|
+
or cp in (0x2705, 0x274C, 0x2714, 0x2716)
|
|
82
|
+
):
|
|
83
|
+
width += 2
|
|
84
|
+
else:
|
|
85
|
+
width += 1
|
|
86
|
+
return width
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# --------------------------------------------------------------------------
|
|
90
|
+
# Tracer
|
|
91
|
+
# --------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
# Module-level so the welcome banner shows exactly once per process,
|
|
94
|
+
# no matter how many Agent/AgentTracer instances get created.
|
|
95
|
+
_WELCOME_SHOWN = False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class AgentTracer:
|
|
99
|
+
"""Renders one Agent run as a live, colorized trace in the terminal.
|
|
100
|
+
|
|
101
|
+
Every method is a no-op if `enabled=False`, so it's cheap to leave
|
|
102
|
+
wired in and toggle via a single flag.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, agent_name: str, enabled: bool = True, stream: t.TextIO = sys.stdout):
|
|
106
|
+
self.agent_name = agent_name
|
|
107
|
+
self.enabled = enabled
|
|
108
|
+
self.stream = stream
|
|
109
|
+
self._color = enabled and _supports_color()
|
|
110
|
+
self._run_started: t.Optional[float] = None
|
|
111
|
+
self._iter_started: t.Optional[float] = None
|
|
112
|
+
|
|
113
|
+
# -- internals --------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def _c(self, code: str) -> str:
|
|
116
|
+
return code if self._color else ""
|
|
117
|
+
|
|
118
|
+
def _print(self, text: str = "") -> None:
|
|
119
|
+
if not self.enabled:
|
|
120
|
+
return
|
|
121
|
+
print(text, file=self.stream, flush=True)
|
|
122
|
+
|
|
123
|
+
def _rule(self, char: str = "─", color: str = "") -> str:
|
|
124
|
+
width = _term_width()
|
|
125
|
+
return f"{self._c(color)}{char * width}{self._c(C.RESET)}"
|
|
126
|
+
|
|
127
|
+
def _box(
|
|
128
|
+
self,
|
|
129
|
+
title: str,
|
|
130
|
+
lines: list[str],
|
|
131
|
+
color: str,
|
|
132
|
+
title_color: str = "",
|
|
133
|
+
center_title: bool = False,
|
|
134
|
+
center_lines: bool = False,
|
|
135
|
+
) -> None:
|
|
136
|
+
width = _term_width()
|
|
137
|
+
inner = width - 4
|
|
138
|
+
title_color = title_color or color
|
|
139
|
+
top = f"{self._c(color)}╭{'─' * (width - 2)}╮{self._c(C.RESET)}"
|
|
140
|
+
bot = f"{self._c(color)}╰{'─' * (width - 2)}╯{self._c(C.RESET)}"
|
|
141
|
+
|
|
142
|
+
if center_title:
|
|
143
|
+
left_pad = max((inner - _display_width(title)) // 2, 0)
|
|
144
|
+
right_pad = max(inner - _display_width(title) - left_pad, 0)
|
|
145
|
+
else:
|
|
146
|
+
left_pad, right_pad = 0, max(inner - _display_width(title), 0)
|
|
147
|
+
|
|
148
|
+
head = (
|
|
149
|
+
f"{self._c(color)}│ {self._c(C.RESET)}{' ' * left_pad}"
|
|
150
|
+
f"{self._c(C.BOLD)}{self._c(title_color)}{title}{self._c(C.RESET)}"
|
|
151
|
+
f"{' ' * right_pad}{self._c(color)} │{self._c(C.RESET)}"
|
|
152
|
+
)
|
|
153
|
+
self._print(top)
|
|
154
|
+
self._print(head)
|
|
155
|
+
if lines:
|
|
156
|
+
self._print(f"{self._c(color)}├{'─' * (width - 2)}┤{self._c(C.RESET)}")
|
|
157
|
+
for raw in lines:
|
|
158
|
+
for wrapped in _wrap(raw, inner) or [""]:
|
|
159
|
+
w = _display_width(wrapped)
|
|
160
|
+
if center_lines:
|
|
161
|
+
lp = max((inner - w) // 2, 0)
|
|
162
|
+
rp = max(inner - w - lp, 0)
|
|
163
|
+
else:
|
|
164
|
+
lp, rp = 0, max(inner - w, 0)
|
|
165
|
+
self._print(
|
|
166
|
+
f"{self._c(color)}│ {self._c(C.RESET)}{' ' * lp}{wrapped}"
|
|
167
|
+
f"{' ' * rp}{self._c(color)} │{self._c(C.RESET)}"
|
|
168
|
+
)
|
|
169
|
+
self._print(bot)
|
|
170
|
+
|
|
171
|
+
# -- lifecycle events ---------------------------------------------------
|
|
172
|
+
|
|
173
|
+
def welcome_banner(self, force: bool = False) -> None:
|
|
174
|
+
"""Prints a one-time 'Welcome to Agentropic' banner. Shown
|
|
175
|
+
automatically before the first run in a process; call with
|
|
176
|
+
force=True to show it again (e.g. at the very start of a script)."""
|
|
177
|
+
global _WELCOME_SHOWN
|
|
178
|
+
if _WELCOME_SHOWN and not force:
|
|
179
|
+
return
|
|
180
|
+
_WELCOME_SHOWN = True
|
|
181
|
+
self._print()
|
|
182
|
+
self._box(
|
|
183
|
+
"🚀 Welcome to Agentropic — Agentic AI System",
|
|
184
|
+
[f"{self._c(C.DIM)}Powered by your agents, tools, and memory.{self._c(C.RESET)}"],
|
|
185
|
+
color=C.MAGENTA,
|
|
186
|
+
center_title=True,
|
|
187
|
+
center_lines=True,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def run_start(self, user_input: str) -> None:
|
|
191
|
+
self.welcome_banner()
|
|
192
|
+
self._run_started = time.monotonic()
|
|
193
|
+
self._print()
|
|
194
|
+
self._box(
|
|
195
|
+
f"🤖 Agent: {self.agent_name}",
|
|
196
|
+
[f"{self._c(C.DIM)}Task ▸{self._c(C.RESET)} {_truncate(user_input, 400)}"],
|
|
197
|
+
color=C.CYAN,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def iteration_start(self, iteration: int, max_iterations: int) -> None:
|
|
201
|
+
self._iter_started = time.monotonic()
|
|
202
|
+
self._print(
|
|
203
|
+
f"\n{self._c(C.BOLD)}{self._c(C.BLUE)}▸ Iteration {iteration}/{max_iterations}"
|
|
204
|
+
f"{self._c(C.RESET)} {self._c(C.GREY)}— calling LLM…{self._c(C.RESET)}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def llm_response(self, content: str, tool_call_names: list[str], duration: float) -> None:
|
|
208
|
+
dur = f"{duration:.2f}s"
|
|
209
|
+
if tool_call_names:
|
|
210
|
+
calls = ", ".join(tool_call_names)
|
|
211
|
+
self._print(
|
|
212
|
+
f" {self._c(C.GREY)}↳ response in {dur}{self._c(C.RESET)} "
|
|
213
|
+
f"{self._c(C.MAGENTA)}requests {len(tool_call_names)} tool call(s):{self._c(C.RESET)} "
|
|
214
|
+
f"{self._c(C.BOLD)}{calls}{self._c(C.RESET)}"
|
|
215
|
+
)
|
|
216
|
+
if content and content.strip():
|
|
217
|
+
self._print(f" {self._c(C.DIM)}💭 {_truncate(content.strip(), 200)}{self._c(C.RESET)}")
|
|
218
|
+
else:
|
|
219
|
+
self._print(f" {self._c(C.GREY)}↳ response in {dur} — final answer{self._c(C.RESET)}")
|
|
220
|
+
|
|
221
|
+
def tool_call_start(self, name: str, arguments: dict) -> None:
|
|
222
|
+
args_str = ", ".join(f"{k}={v!r}" for k, v in (arguments or {}).items())
|
|
223
|
+
self._print(
|
|
224
|
+
f" {self._c(C.YELLOW)}🔧 {self._c(C.BOLD)}{name}{self._c(C.RESET)}"
|
|
225
|
+
f"{self._c(C.GREY)}({_truncate(args_str, 140)}){self._c(C.RESET)}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def tool_call_end(self, name: str, result: str, duration: float, error: bool) -> None:
|
|
229
|
+
icon = "❌" if error else "✅"
|
|
230
|
+
color = C.RED if error else C.GREEN
|
|
231
|
+
self._print(
|
|
232
|
+
f" {self._c(color)}{icon} {name} → {duration:.2f}s{self._c(C.RESET)} "
|
|
233
|
+
f"{self._c(C.DIM)}{_truncate(str(result).replace(chr(10), ' '), 160)}{self._c(C.RESET)}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def final_answer(self, output: str, iterations: int, tool_call_count: int, duration: float, usage: dict) -> None:
|
|
237
|
+
stats = (
|
|
238
|
+
f"{self._c(C.DIM)}⏱ {duration:.2f}s 🔁 {iterations} iteration(s) "
|
|
239
|
+
f"🔧 {tool_call_count} tool call(s)"
|
|
240
|
+
)
|
|
241
|
+
if usage and usage.get("total_tokens"):
|
|
242
|
+
stats += f" 🎫 {usage['total_tokens']} tokens"
|
|
243
|
+
stats += self._c(C.RESET)
|
|
244
|
+
self._print()
|
|
245
|
+
self._box(
|
|
246
|
+
"✅ Final Answer",
|
|
247
|
+
[output, "", stats],
|
|
248
|
+
color=C.GREEN,
|
|
249
|
+
)
|
|
250
|
+
self._print()
|
|
251
|
+
|
|
252
|
+
def max_iterations_error(self, max_iterations: int) -> None:
|
|
253
|
+
self._print()
|
|
254
|
+
self._box(
|
|
255
|
+
"⚠️ Max iterations reached",
|
|
256
|
+
[f"Agent '{self.agent_name}' hit max_iterations ({max_iterations}) without a final answer."],
|
|
257
|
+
color=C.RED,
|
|
258
|
+
)
|
|
259
|
+
self._print()
|
|
260
|
+
|
|
261
|
+
def error(self, message: str) -> None:
|
|
262
|
+
self._print(f" {self._c(C.RED)}{self._c(C.BOLD)}✖ {message}{self._c(C.RESET)}")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class NullTracer(AgentTracer):
|
|
266
|
+
"""Same interface, does nothing — used when verbose=False."""
|
|
267
|
+
|
|
268
|
+
def __init__(self, agent_name: str = ""):
|
|
269
|
+
super().__init__(agent_name, enabled=False)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|