-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcli_advanced_observer.py
More file actions
141 lines (113 loc) · 3.88 KB
/
Copy pathcli_advanced_observer.py
File metadata and controls
141 lines (113 loc) · 3.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""Event-observer demo: watch every event type flow from a single run.
Runs fully offline with a scripted model, so no API key is needed:
uv run python examples/cli_advanced_observer.py
Iterates the streaming event API directly and prints each TextDelta, tool call,
tool result, step, and the final event. It also wires a context_manager that
trims the prompt and sets a model_timeout budget, two of the production knobs.
"""
from __future__ import annotations
import asyncio
import json
from collections.abc import AsyncIterator, Sequence
from typing import Any
from agentling import (
Agent,
ChatMessage,
Delta,
FinalEvent,
StepEvent,
TextDelta,
ToolCall,
ToolCallDelta,
ToolCallEvent,
ToolResultEvent,
Usage,
tool,
)
@tool
def health_check() -> str:
"""Report a one-line service health status."""
return "all systems nominal"
class ScriptedModel:
"""A deterministic offline model that replays fixed assistant turns."""
def __init__(self, turns: Sequence[ChatMessage]) -> None:
self._turns = list(turns)
self._index = 0
async def generate(
self, messages: Sequence[ChatMessage], tools: Sequence[Any] | None = None
) -> ChatMessage:
turn = self._turns[self._index]
self._index += 1
return turn
async def stream(
self, messages: Sequence[ChatMessage], tools: Sequence[Any] | None = None
) -> AsyncIterator[Delta]:
turn = self._turns[self._index]
self._index += 1
if turn.content:
yield Delta(content=turn.content)
for index, call in enumerate(turn.tool_calls):
yield Delta(
tool_calls=[
ToolCallDelta(
index=index,
id=call.id,
name=call.name,
arguments=json.dumps(call.arguments),
)
]
)
yield Delta(usage=turn.usage)
def _assistant(
content: str = "", tool_calls: list[ToolCall] | None = None
) -> ChatMessage:
return ChatMessage(
role="assistant",
content=content,
tool_calls=tool_calls or [],
usage=Usage(1, 1),
)
def keep_recent(messages: list[ChatMessage]) -> list[ChatMessage]:
"""A context_manager that keeps the system prompt and the recent tail."""
if len(messages) <= 6:
return messages
return [messages[0], *messages[-5:]]
def build_agent() -> Agent:
"""Build an offline agent that calls one tool, then answers."""
model = ScriptedModel(
[
_assistant(
tool_calls=[ToolCall(id="c1", name="health_check", arguments={})]
),
_assistant(content="All systems nominal."),
]
)
return Agent(
model=model,
tools=[health_check],
context_manager=keep_recent,
model_timeout=30.0,
)
async def observe(agent: Agent) -> dict[str, int]:
"""Stream a run, print each event, and tally how many of each type arrived."""
counts: dict[str, int] = {}
session = agent.start()
async for event in session.run("Check system health.", stream=True):
counts[type(event).__name__] = counts.get(type(event).__name__, 0) + 1
match event:
case TextDelta(text=text):
print(text, end="", flush=True)
case ToolCallEvent(tool_call=call):
print(f"\n-> {call.name}()")
case ToolResultEvent(result=result):
print(f"<- {result.content}")
case StepEvent():
print("[step recorded]")
case FinalEvent(answer=answer, status=status):
print(f"\n= [{status}] {answer}")
return counts
async def main() -> None:
counts = await observe(build_agent())
print("\nEvent counts:", counts)
if __name__ == "__main__":
asyncio.run(main())