connectonion 0.6.0__py3-none-any.whl → 0.6.1__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.
- connectonion/__init__.py +3 -2
- connectonion/cli/browser_agent/browser.py +488 -145
- connectonion/cli/browser_agent/scroll_strategies.py +276 -0
- connectonion/cli/commands/eval_commands.py +286 -0
- connectonion/cli/main.py +11 -0
- connectonion/console.py +5 -5
- connectonion/core/agent.py +13 -10
- connectonion/core/llm.py +9 -19
- connectonion/logger.py +305 -135
- connectonion/network/__init__.py +3 -0
- connectonion/network/asgi.py +122 -2
- connectonion/network/connection.py +123 -0
- connectonion/network/host.py +7 -5
- connectonion/useful_plugins/__init__.py +4 -3
- connectonion/useful_plugins/ui_stream.py +164 -0
- {connectonion-0.6.0.dist-info → connectonion-0.6.1.dist-info}/METADATA +1 -1
- {connectonion-0.6.0.dist-info → connectonion-0.6.1.dist-info}/RECORD +20 -16
- /connectonion/{static → network/static}/docs.html +0 -0
- {connectonion-0.6.0.dist-info → connectonion-0.6.1.dist-info}/WHEEL +0 -0
- {connectonion-0.6.0.dist-info → connectonion-0.6.1.dist-info}/entry_points.txt +0 -0
connectonion/network/asgi.py
CHANGED
|
@@ -6,13 +6,48 @@ requests. Separated from host.py for better testing and smaller file size.
|
|
|
6
6
|
Design decision: Raw ASGI instead of Starlette/FastAPI for full protocol control.
|
|
7
7
|
See: docs/design-decisions/022-raw-asgi-implementation.md
|
|
8
8
|
"""
|
|
9
|
+
import asyncio
|
|
9
10
|
import hmac
|
|
10
11
|
import json
|
|
11
12
|
import os
|
|
13
|
+
import queue
|
|
14
|
+
import threading
|
|
12
15
|
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict
|
|
13
17
|
|
|
14
18
|
from pydantic import BaseModel
|
|
15
19
|
|
|
20
|
+
from .connection import Connection
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncToSyncConnection(Connection):
|
|
24
|
+
"""Bridge async WebSocket to sync Connection interface.
|
|
25
|
+
|
|
26
|
+
Uses queues to communicate between async WebSocket handler and sync agent code.
|
|
27
|
+
The agent runs in a thread, sending/receiving via queues.
|
|
28
|
+
The async handler pumps messages between WebSocket and queues.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self._outgoing: queue.Queue[Dict[str, Any]] = queue.Queue()
|
|
33
|
+
self._incoming: queue.Queue[Dict[str, Any]] = queue.Queue()
|
|
34
|
+
self._closed = False
|
|
35
|
+
|
|
36
|
+
def send(self, event: Dict[str, Any]) -> None:
|
|
37
|
+
"""Queue event to be sent to client."""
|
|
38
|
+
if not self._closed:
|
|
39
|
+
self._outgoing.put(event)
|
|
40
|
+
|
|
41
|
+
def receive(self) -> Dict[str, Any]:
|
|
42
|
+
"""Block until response from client."""
|
|
43
|
+
return self._incoming.get()
|
|
44
|
+
|
|
45
|
+
def close(self):
|
|
46
|
+
"""Mark connection as closed."""
|
|
47
|
+
self._closed = True
|
|
48
|
+
# Unblock any waiting receive
|
|
49
|
+
self._incoming.put({"type": "connection_closed"})
|
|
50
|
+
|
|
16
51
|
|
|
17
52
|
def _json_default(obj):
|
|
18
53
|
"""Handle non-serializable objects like Pydantic models.
|
|
@@ -190,6 +225,11 @@ async def handle_websocket(
|
|
|
190
225
|
):
|
|
191
226
|
"""Handle WebSocket connections at /ws.
|
|
192
227
|
|
|
228
|
+
Supports bidirectional communication via Connection interface:
|
|
229
|
+
- Agent sends events via connection.log() / connection.send()
|
|
230
|
+
- Agent requests approval via connection.request_approval()
|
|
231
|
+
- Client responds to approval requests
|
|
232
|
+
|
|
193
233
|
Args:
|
|
194
234
|
scope: ASGI scope dict
|
|
195
235
|
receive: ASGI receive callable
|
|
@@ -229,9 +269,89 @@ async def handle_websocket(
|
|
|
229
269
|
await send({"type": "websocket.send",
|
|
230
270
|
"text": json.dumps({"type": "ERROR", "message": "prompt required"})})
|
|
231
271
|
continue
|
|
232
|
-
|
|
272
|
+
|
|
273
|
+
# Create connection for bidirectional communication
|
|
274
|
+
connection = AsyncToSyncConnection()
|
|
275
|
+
agent_done = threading.Event()
|
|
276
|
+
result_holder = [None]
|
|
277
|
+
|
|
278
|
+
def run_agent():
|
|
279
|
+
result_holder[0] = handlers["ws_input"](prompt, connection)
|
|
280
|
+
agent_done.set()
|
|
281
|
+
|
|
282
|
+
# Start agent in thread
|
|
283
|
+
agent_thread = threading.Thread(target=run_agent, daemon=True)
|
|
284
|
+
agent_thread.start()
|
|
285
|
+
|
|
286
|
+
# Pump messages between WebSocket and connection
|
|
287
|
+
await _pump_messages(receive, send, connection, agent_done)
|
|
288
|
+
|
|
289
|
+
# Send final result
|
|
233
290
|
await send({"type": "websocket.send",
|
|
234
|
-
"text": json.dumps({"type": "OUTPUT", "result":
|
|
291
|
+
"text": json.dumps({"type": "OUTPUT", "result": result_holder[0]})})
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
async def _pump_messages(ws_receive, ws_send, connection: AsyncToSyncConnection, agent_done: threading.Event):
|
|
295
|
+
"""Pump messages between WebSocket and connection queues.
|
|
296
|
+
|
|
297
|
+
Runs until agent completes. Handles:
|
|
298
|
+
- Outgoing: connection._outgoing queue → WebSocket
|
|
299
|
+
- Incoming: WebSocket → connection._incoming queue (for approval responses)
|
|
300
|
+
"""
|
|
301
|
+
loop = asyncio.get_event_loop()
|
|
302
|
+
|
|
303
|
+
async def send_outgoing():
|
|
304
|
+
"""Send outgoing messages from connection to WebSocket."""
|
|
305
|
+
while not agent_done.is_set():
|
|
306
|
+
# Use run_in_executor for blocking queue.get
|
|
307
|
+
try:
|
|
308
|
+
event = await loop.run_in_executor(
|
|
309
|
+
None, lambda: connection._outgoing.get(timeout=0.05)
|
|
310
|
+
)
|
|
311
|
+
await ws_send({"type": "websocket.send", "text": json.dumps(event)})
|
|
312
|
+
except queue.Empty:
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
# Drain remaining
|
|
316
|
+
while True:
|
|
317
|
+
try:
|
|
318
|
+
event = connection._outgoing.get_nowait()
|
|
319
|
+
await ws_send({"type": "websocket.send", "text": json.dumps(event)})
|
|
320
|
+
except queue.Empty:
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
async def receive_incoming():
|
|
324
|
+
"""Receive incoming messages from WebSocket to connection."""
|
|
325
|
+
while not agent_done.is_set():
|
|
326
|
+
try:
|
|
327
|
+
msg = await asyncio.wait_for(ws_receive(), timeout=0.1)
|
|
328
|
+
if msg["type"] == "websocket.receive":
|
|
329
|
+
try:
|
|
330
|
+
data = json.loads(msg.get("text", "{}"))
|
|
331
|
+
connection._incoming.put(data)
|
|
332
|
+
except json.JSONDecodeError:
|
|
333
|
+
pass
|
|
334
|
+
elif msg["type"] == "websocket.disconnect":
|
|
335
|
+
connection.close()
|
|
336
|
+
break
|
|
337
|
+
except asyncio.TimeoutError:
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
# Run both tasks concurrently
|
|
341
|
+
send_task = asyncio.create_task(send_outgoing())
|
|
342
|
+
recv_task = asyncio.create_task(receive_incoming())
|
|
343
|
+
|
|
344
|
+
# Wait for agent to complete
|
|
345
|
+
while not agent_done.is_set():
|
|
346
|
+
await asyncio.sleep(0.05)
|
|
347
|
+
|
|
348
|
+
# Cancel receive task and wait for send to finish draining
|
|
349
|
+
recv_task.cancel()
|
|
350
|
+
try:
|
|
351
|
+
await recv_task
|
|
352
|
+
except asyncio.CancelledError:
|
|
353
|
+
pass
|
|
354
|
+
await send_task
|
|
235
355
|
|
|
236
356
|
|
|
237
357
|
def create_app(
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Connection interface for agent-client communication during hosted execution
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [abc, typing] | imported by [asgi.py, __init__.py] | tested by [tests/unit/test_connection.py]
|
|
5
|
+
Data flow: receives from host/asgi → WebSocket send/receive → provides log() and request_approval() to agent event handlers
|
|
6
|
+
State/Effects: stateless base class | WebSocketConnection wraps ASGI WebSocket for bidirectional messaging
|
|
7
|
+
Integration: exposes Connection (base), WebSocketConnection (ASGI adapter) | agent.connection set by host() during WebSocket requests
|
|
8
|
+
Performance: log() is fire-and-forget (non-blocking) | request_approval() blocks waiting for client response
|
|
9
|
+
Errors: WebSocketConnection raises if WebSocket closed unexpectedly
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Connection(ABC):
|
|
17
|
+
"""Base connection interface for agent-client communication.
|
|
18
|
+
|
|
19
|
+
Two-layer API:
|
|
20
|
+
- Low-level: send(event), receive() - primitives for any communication
|
|
21
|
+
- High-level: log(type, **data), request_approval(tool, args) - common patterns
|
|
22
|
+
|
|
23
|
+
Usage in event handlers:
|
|
24
|
+
@after_llm
|
|
25
|
+
def on_thinking(agent):
|
|
26
|
+
if agent.connection:
|
|
27
|
+
agent.connection.log("thinking")
|
|
28
|
+
|
|
29
|
+
@before_each_tool
|
|
30
|
+
def on_tool(agent):
|
|
31
|
+
if agent.connection:
|
|
32
|
+
tool = agent.current_session['pending_tool']
|
|
33
|
+
if tool['name'] in DANGEROUS:
|
|
34
|
+
if not agent.connection.request_approval(tool['name'], tool['arguments']):
|
|
35
|
+
raise ToolRejected()
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# ═══════════════════════════════════════════════════════
|
|
39
|
+
# LOW-LEVEL API (Primitives)
|
|
40
|
+
# ═══════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def send(self, event: Dict[str, Any]) -> None:
|
|
44
|
+
"""Send any event to client.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
event: Dict with at least 'type' key, e.g. {"type": "thinking"}
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def receive(self) -> Dict[str, Any]:
|
|
53
|
+
"""Receive response from client.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dict response from client
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
# ═══════════════════════════════════════════════════════
|
|
61
|
+
# HIGH-LEVEL API (Patterns)
|
|
62
|
+
# ═══════════════════════════════════════════════════════
|
|
63
|
+
|
|
64
|
+
def log(self, event_type: str, **data) -> None:
|
|
65
|
+
"""One-way notification to client.
|
|
66
|
+
|
|
67
|
+
Common event types: thinking, tool_call, tool_result, complete, error
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
event_type: Type of event (e.g. "thinking", "tool_call")
|
|
71
|
+
**data: Additional data for the event
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
connection.log("thinking")
|
|
75
|
+
connection.log("tool_call", name="search", arguments={"q": "python"})
|
|
76
|
+
"""
|
|
77
|
+
self.send({"type": event_type, **data})
|
|
78
|
+
|
|
79
|
+
def request_approval(self, tool: str, arguments: Dict[str, Any]) -> bool:
|
|
80
|
+
"""Two-way: request permission, wait for response.
|
|
81
|
+
|
|
82
|
+
Sends approval_needed event and blocks until client responds.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
tool: Name of tool requiring approval
|
|
86
|
+
arguments: Tool arguments to show user
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if approved, False if rejected
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
if not connection.request_approval("delete_file", {"path": "/tmp/x"}):
|
|
93
|
+
raise ToolRejected()
|
|
94
|
+
"""
|
|
95
|
+
self.send({"type": "approval_needed", "tool": tool, "arguments": arguments})
|
|
96
|
+
response = self.receive()
|
|
97
|
+
return response.get("approved", False)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SyncWebSocketConnection(Connection):
|
|
101
|
+
"""Synchronous WebSocket connection adapter.
|
|
102
|
+
|
|
103
|
+
Wraps async WebSocket send/receive for use in synchronous agent code.
|
|
104
|
+
Uses threading events to bridge async/sync boundary.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, send_callback, receive_callback):
|
|
108
|
+
"""Initialize with send/receive callbacks.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
send_callback: Callable that sends message to WebSocket
|
|
112
|
+
receive_callback: Callable that receives message from WebSocket
|
|
113
|
+
"""
|
|
114
|
+
self._send = send_callback
|
|
115
|
+
self._receive = receive_callback
|
|
116
|
+
|
|
117
|
+
def send(self, event: Dict[str, Any]) -> None:
|
|
118
|
+
"""Send event to client via WebSocket."""
|
|
119
|
+
self._send(event)
|
|
120
|
+
|
|
121
|
+
def receive(self) -> Dict[str, Any]:
|
|
122
|
+
"""Receive response from client via WebSocket."""
|
|
123
|
+
return self._receive()
|
connectonion/network/host.py
CHANGED
|
@@ -178,7 +178,7 @@ def health_handler(agent, start_time: float) -> dict:
|
|
|
178
178
|
def info_handler(agent, trust: str) -> dict:
|
|
179
179
|
"""GET /info"""
|
|
180
180
|
from .. import __version__
|
|
181
|
-
tools = agent.tools.
|
|
181
|
+
tools = agent.tools.names() if hasattr(agent.tools, "names") else []
|
|
182
182
|
return {
|
|
183
183
|
"name": agent.name,
|
|
184
184
|
"address": get_agent_address(agent),
|
|
@@ -382,7 +382,7 @@ def admin_sessions_handler() -> dict:
|
|
|
382
382
|
Frontend handles the display logic.
|
|
383
383
|
"""
|
|
384
384
|
import yaml
|
|
385
|
-
sessions_dir = Path(".co/
|
|
385
|
+
sessions_dir = Path(".co/evals")
|
|
386
386
|
if not sessions_dir.exists():
|
|
387
387
|
return {"sessions": []}
|
|
388
388
|
|
|
@@ -407,9 +407,11 @@ def _create_handlers(agent_template, result_ttl: int):
|
|
|
407
407
|
agent_template: Agent used as template (deep-copied per request for isolation)
|
|
408
408
|
result_ttl: How long to keep results on server in seconds
|
|
409
409
|
"""
|
|
410
|
-
def ws_input(prompt: str) -> str:
|
|
411
|
-
|
|
412
|
-
|
|
410
|
+
def ws_input(prompt: str, connection) -> str:
|
|
411
|
+
"""WebSocket input with bidirectional connection support."""
|
|
412
|
+
agent_template.reset_conversation()
|
|
413
|
+
agent_template.connection = connection
|
|
414
|
+
return agent_template.input(prompt)
|
|
413
415
|
|
|
414
416
|
return {
|
|
415
417
|
"input": lambda storage, prompt, ttl, session=None: input_handler(agent_template, storage, prompt, ttl, session),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Purpose: Export pre-built plugins that extend agent behavior via event hooks
|
|
3
3
|
LLM-Note:
|
|
4
|
-
Dependencies: imports from [re_act, image_result_formatter, shell_approval, gmail_plugin, calendar_plugin] | imported by [__init__.py main package] | re-exports plugins for agent consumption
|
|
4
|
+
Dependencies: imports from [re_act, image_result_formatter, shell_approval, gmail_plugin, calendar_plugin, ui_stream] | imported by [__init__.py main package] | re-exports plugins for agent consumption
|
|
5
5
|
Data flow: agent imports plugin → passes to Agent(plugins=[plugin]) → plugin event handlers fire on agent lifecycle events
|
|
6
6
|
State/Effects: no state | pure re-exports | plugins modify agent behavior at runtime
|
|
7
|
-
Integration: exposes re_act (ReAct prompting), image_result_formatter (base64 image handling), shell_approval (user confirmation for shell commands), gmail_plugin (Gmail OAuth flow), calendar_plugin (Google Calendar integration) | plugins are lists of event handlers
|
|
7
|
+
Integration: exposes re_act (ReAct prompting), image_result_formatter (base64 image handling), shell_approval (user confirmation for shell commands), gmail_plugin (Gmail OAuth flow), calendar_plugin (Google Calendar integration), ui_stream (WebSocket event streaming) | plugins are lists of event handlers
|
|
8
8
|
Errors: ImportError if underlying plugin dependencies not installed
|
|
9
9
|
|
|
10
10
|
Pre-built plugins that can be easily imported and used across agents.
|
|
@@ -16,5 +16,6 @@ from .image_result_formatter import image_result_formatter
|
|
|
16
16
|
from .shell_approval import shell_approval
|
|
17
17
|
from .gmail_plugin import gmail_plugin
|
|
18
18
|
from .calendar_plugin import calendar_plugin
|
|
19
|
+
from .ui_stream import ui_stream
|
|
19
20
|
|
|
20
|
-
__all__ = ['re_act', 'eval', 'image_result_formatter', 'shell_approval', 'gmail_plugin', 'calendar_plugin']
|
|
21
|
+
__all__ = ['re_act', 'eval', 'image_result_formatter', 'shell_approval', 'gmail_plugin', 'calendar_plugin', 'ui_stream']
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Stream agent activity to UI via Connection API
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [typing, core.events] | imported by [useful_plugins/__init__.py] | tested by [tests/unit/test_ui_stream.py]
|
|
5
|
+
Data flow: Event handlers check agent.connection → if present, call connection.log() to stream events
|
|
6
|
+
State/Effects: Streams events to WebSocket client | no file I/O | no blocking
|
|
7
|
+
Integration: exposes ui_stream list of event handlers | used via Agent(plugins=[ui_stream]) | works with host() WebSocket
|
|
8
|
+
Performance: O(1) event dispatch | no blocking | no LLM calls
|
|
9
|
+
Errors: Silently skips if connection is None (local execution)
|
|
10
|
+
|
|
11
|
+
UI Stream Plugin - Stream agent activity to connected UI clients.
|
|
12
|
+
|
|
13
|
+
When agent.connection is set (by host() for WebSocket connections), this plugin
|
|
14
|
+
streams real-time events for:
|
|
15
|
+
- LLM responses (thinking, tool_calls)
|
|
16
|
+
- Tool execution (before/after each tool)
|
|
17
|
+
- Errors and completion
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
from connectonion import Agent
|
|
21
|
+
from connectonion.useful_plugins import ui_stream
|
|
22
|
+
|
|
23
|
+
agent = Agent("assistant", plugins=[ui_stream])
|
|
24
|
+
host(agent) # WebSocket clients receive real-time events
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
from ..core.events import (
|
|
29
|
+
after_llm,
|
|
30
|
+
before_each_tool,
|
|
31
|
+
after_each_tool,
|
|
32
|
+
on_error,
|
|
33
|
+
on_complete,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from ..core.agent import Agent
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@after_llm
|
|
41
|
+
def stream_llm_response(agent: 'Agent') -> None:
|
|
42
|
+
"""Stream LLM response to connected UI."""
|
|
43
|
+
if not agent.connection:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
trace = agent.current_session.get('trace', [])
|
|
47
|
+
if not trace:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
last = trace[-1]
|
|
51
|
+
if last.get('type') != 'llm_call':
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# Stream content if present
|
|
55
|
+
content = last.get('content', '')
|
|
56
|
+
if content:
|
|
57
|
+
agent.connection.log('message', content=content)
|
|
58
|
+
|
|
59
|
+
# Stream tool calls if present
|
|
60
|
+
tool_calls = last.get('tool_calls', [])
|
|
61
|
+
for tc in tool_calls:
|
|
62
|
+
agent.connection.log(
|
|
63
|
+
'tool_pending',
|
|
64
|
+
name=tc.get('function', {}).get('name', ''),
|
|
65
|
+
arguments=tc.get('function', {}).get('arguments', {}),
|
|
66
|
+
id=tc.get('id', ''),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@before_each_tool
|
|
71
|
+
def stream_tool_start(agent: 'Agent') -> None:
|
|
72
|
+
"""Stream tool execution start to connected UI."""
|
|
73
|
+
if not agent.connection:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
pending = agent.current_session.get('pending_tool')
|
|
77
|
+
if not pending:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
agent.connection.log(
|
|
81
|
+
'tool_start',
|
|
82
|
+
name=pending['name'],
|
|
83
|
+
arguments=pending['arguments'],
|
|
84
|
+
id=pending.get('id', ''),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@after_each_tool
|
|
89
|
+
def stream_tool_result(agent: 'Agent') -> None:
|
|
90
|
+
"""Stream tool execution result to connected UI."""
|
|
91
|
+
if not agent.connection:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
trace = agent.current_session.get('trace', [])
|
|
95
|
+
if not trace:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
last = trace[-1]
|
|
99
|
+
if last.get('type') != 'tool_execution':
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
status = last.get('status', 'unknown')
|
|
103
|
+
result = last.get('result', '')
|
|
104
|
+
|
|
105
|
+
# Truncate large results for UI
|
|
106
|
+
if isinstance(result, str) and len(result) > 1000:
|
|
107
|
+
result = result[:1000] + '...'
|
|
108
|
+
|
|
109
|
+
agent.connection.log(
|
|
110
|
+
'tool_result',
|
|
111
|
+
name=last.get('tool_name', ''),
|
|
112
|
+
status=status,
|
|
113
|
+
result=result,
|
|
114
|
+
timing_ms=last.get('timing', 0),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@on_error
|
|
119
|
+
def stream_error(agent: 'Agent') -> None:
|
|
120
|
+
"""Stream error to connected UI."""
|
|
121
|
+
if not agent.connection:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
trace = agent.current_session.get('trace', [])
|
|
125
|
+
if not trace:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
last = trace[-1]
|
|
129
|
+
if last.get('status') != 'error':
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
agent.connection.log(
|
|
133
|
+
'error',
|
|
134
|
+
tool_name=last.get('tool_name', ''),
|
|
135
|
+
error=str(last.get('error', '')),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@on_complete
|
|
140
|
+
def stream_complete(agent: 'Agent') -> None:
|
|
141
|
+
"""Stream completion to connected UI."""
|
|
142
|
+
if not agent.connection:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
trace = agent.current_session.get('trace', [])
|
|
146
|
+
tools_used = [t.get('tool_name', '') for t in trace if t.get('type') == 'tool_execution']
|
|
147
|
+
llm_calls = len([t for t in trace if t.get('type') == 'llm_call'])
|
|
148
|
+
|
|
149
|
+
agent.connection.log(
|
|
150
|
+
'complete',
|
|
151
|
+
tools_used=tools_used,
|
|
152
|
+
llm_calls=llm_calls,
|
|
153
|
+
iterations=agent.current_session.get('iteration', 0),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Bundle as plugin
|
|
158
|
+
ui_stream = [
|
|
159
|
+
stream_llm_response,
|
|
160
|
+
stream_tool_start,
|
|
161
|
+
stream_tool_result,
|
|
162
|
+
stream_error,
|
|
163
|
+
stream_complete,
|
|
164
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: connectonion
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: A simple Python framework for creating AI agents with behavior tracking
|
|
5
5
|
Project-URL: Homepage, https://github.com/openonion/connectonion
|
|
6
6
|
Project-URL: Documentation, https://docs.connectonion.com
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
connectonion/__init__.py,sha256=
|
|
1
|
+
connectonion/__init__.py,sha256=7geVck4xTtsy0x9UbmCpSuFaCzaTTiT5PgHHXIGUKLU,1958
|
|
2
2
|
connectonion/address.py,sha256=YOzpMOej-HqJUE6o0i0fG8rB7HM-Iods36s9OD--5ig,10852
|
|
3
|
-
connectonion/console.py,sha256=
|
|
3
|
+
connectonion/console.py,sha256=zmnhavw359iqY9HFI1gbZolRSqiDlzJPEkwI1CllWfY,21239
|
|
4
4
|
connectonion/llm_do.py,sha256=rwgSsTreNGAq5xV3m9lbA7U5AE0XOZNdihJwW5FHz0k,12005
|
|
5
|
-
connectonion/logger.py,sha256=
|
|
5
|
+
connectonion/logger.py,sha256=AtyMuBUB7zbCuAQcySMZpRJ9qdAwU5q0V61mLVSx7Ec,17660
|
|
6
6
|
connectonion/prompts.py,sha256=hmbx8ACOJzystbRPQ6bMqx8BQKBKJHNJnMogtAZgDGA,5661
|
|
7
7
|
connectonion/transcribe.py,sha256=m2qd7A2qMKFAbmbLLgvcd-yUFz8FHgjUKtvtFffnK00,7730
|
|
8
8
|
connectonion/cli/__init__.py,sha256=Pd1iKp0rUghs2kmdAhyp9hK8j0uWkNxOagBkdCGrG4I,55
|
|
9
9
|
connectonion/cli/docs.md,sha256=Fk_JT8jFXXDpXTvU0ZUigMW1UR6ERmX-HDheYPPRNY8,3231
|
|
10
|
-
connectonion/cli/main.py,sha256=
|
|
10
|
+
connectonion/cli/main.py,sha256=ys0L_zwV4lkrln_86XrJ_U_1ARQ6oxoADtHWj3tSIkg,7315
|
|
11
11
|
connectonion/cli/browser_agent/__init__.py,sha256=xZCoxS3dGVBBMnaasHOE1vxMDdIwUloRP67rGR-4obo,173
|
|
12
|
-
connectonion/cli/browser_agent/browser.py,sha256=
|
|
12
|
+
connectonion/cli/browser_agent/browser.py,sha256=P3ceJrHVS63NJvuQltjeOy4rZj1H-8kDy-qOZSWK9AA,21532
|
|
13
13
|
connectonion/cli/browser_agent/prompt.md,sha256=tF0PhGcl3gkmVK1F5SG9GZwnPPXTZYWEaatAZtOYZZg,4479
|
|
14
|
+
connectonion/cli/browser_agent/scroll_strategies.py,sha256=t_noSlzbEptgBiRWgrlIv4L_jv33xHvKgxbg1qUFDL4,10109
|
|
14
15
|
connectonion/cli/commands/__init__.py,sha256=IPZy9NwrE0hs4f7hIqyFM9GjFZpeCS8mC0Jf-5Ooy4c,43
|
|
15
16
|
connectonion/cli/commands/auth_commands.py,sha256=D76_0yd77d23bXRvsTAY6HOcGJswo9-6z2dRi9CR9sE,21635
|
|
16
17
|
connectonion/cli/commands/browser_commands.py,sha256=lB4N6XwP43qdcthwQWlbFU2S3INfxhRDXznAw1PSTsQ,1803
|
|
@@ -18,6 +19,7 @@ connectonion/cli/commands/copy_commands.py,sha256=f59STrC4Yy3OtYiUSz99K0zkU3tiWo
|
|
|
18
19
|
connectonion/cli/commands/create.py,sha256=0TrsPulS6dqwr_gtIWWBQ03Q_BnNro-CV4qOV5VkHAg,22011
|
|
19
20
|
connectonion/cli/commands/deploy_commands.py,sha256=l-o6XDxh5kDB0YUCb_LeVERvdUY4gUogOlNalRv1OdM,8774
|
|
20
21
|
connectonion/cli/commands/doctor_commands.py,sha256=EOk8CMclvVqLq4-Dg2JghWehH9VQnthBejrhIBX66_4,7244
|
|
22
|
+
connectonion/cli/commands/eval_commands.py,sha256=PwWEtnfLZqkgWzFF9Fa1bHkf6TxmIf6D8_M9D6rwuJA,10012
|
|
21
23
|
connectonion/cli/commands/init.py,sha256=8zpY01Ux25BM6UCMvs66DRtiHKHsq7Yo9N_KOhAhOOA,20979
|
|
22
24
|
connectonion/cli/commands/project_cmd_lib.py,sha256=SfqTpbQmYgMiUdi94Rkn1jF6hIlEnfgV0cSWqxUhJiw,32197
|
|
23
25
|
connectonion/cli/commands/reset_commands.py,sha256=FkK6tMn7QOY3X5ulTYQ7VnLfeYpStgbZ5UnnbD3zudY,7016
|
|
@@ -38,9 +40,9 @@ connectonion/cli/templates/playwright/prompt.md,sha256=wsvpWTzxrWYs4t4WLhXgJ7Auk
|
|
|
38
40
|
connectonion/cli/templates/playwright/requirements.txt,sha256=oSyGEC1-DyWm3_vE1QkREeWx4odSlS-K2UjlUCTw4tM,38
|
|
39
41
|
connectonion/cli/templates/web-research/agent.py,sha256=PS50oxPg8GX8RIjjZ6iSVhKDrS00riCjTPSrP3RIFV0,3970
|
|
40
42
|
connectonion/core/__init__.py,sha256=NIUXpkxalSzbchqklnaxUsG-X5ZUNO4wdZ3lOwwzb_Q,1363
|
|
41
|
-
connectonion/core/agent.py,sha256=
|
|
43
|
+
connectonion/core/agent.py,sha256=RXd-0DOWZg_u6iJXMPYlg4oH45BlbH5PULLWtB32h38,18944
|
|
42
44
|
connectonion/core/events.py,sha256=jJOMt1PhRl-ef4R8-WpAq8pUbZ8GKIl0wOB2kJUVyWg,9151
|
|
43
|
-
connectonion/core/llm.py,sha256=
|
|
45
|
+
connectonion/core/llm.py,sha256=i-jCvaryIPGqYx7SXR5yuhh64W4DdlnmCqklESYpg78,32425
|
|
44
46
|
connectonion/core/tool_executor.py,sha256=YGvgJpkFPFIxfFEVeASfvkYLreH6kXeTFoVZ_IqVJPU,10690
|
|
45
47
|
connectonion/core/tool_factory.py,sha256=j0DoRuHoVqx8ej07c8nOOyHLPzSzCah1D3mY4P9I8yg,6853
|
|
46
48
|
connectonion/core/tool_registry.py,sha256=rTe8RJnWXpmHXWznP468fhWvmRknk-TlF9Iz8G9_qus,4367
|
|
@@ -63,22 +65,23 @@ connectonion/debug/runtime_inspector/__init__.py,sha256=6Hf88LTZ1iEAnNzfTkjp7iRb
|
|
|
63
65
|
connectonion/debug/runtime_inspector/agent.py,sha256=kGXncq8AOffUQ_fuj9Sw15of6_mtsvrsYNZ5-lU74ao,2510
|
|
64
66
|
connectonion/debug/runtime_inspector/runtime_inspector.py,sha256=rxFOqTxSvyKxqagYL4V2Pf-ii0Ie8l14a5xHzgIb810,15336
|
|
65
67
|
connectonion/debug/runtime_inspector/prompts/debug_assistant.md,sha256=sR17NHwBY-8EgIZwK4AoTKW9D6vEDUf1Aq7i936tqco,2816
|
|
66
|
-
connectonion/network/__init__.py,sha256=
|
|
68
|
+
connectonion/network/__init__.py,sha256=S8p7lvq74WpCrVNTI_X3myZgFecjdq93Xdve3XwyWII,1037
|
|
67
69
|
connectonion/network/announce.py,sha256=roS_PIYef7g-HlXhIkUrCEtGNUDBEStqFodCU8qEy5M,3366
|
|
68
|
-
connectonion/network/asgi.py,sha256=
|
|
70
|
+
connectonion/network/asgi.py,sha256=F1aF9lzaA78I_2mCWngQ0oTVtEGEukP-viXLBs2hmDM,13909
|
|
69
71
|
connectonion/network/connect.py,sha256=w6moVt1alBZjHoGgHzaWMf9SgHVPlN1drO4lqWeYWyY,9795
|
|
70
|
-
connectonion/network/
|
|
72
|
+
connectonion/network/connection.py,sha256=HqaENrlAh2RCNig8ntte2Nxw4dUT4pz2xNu400bRZeM,4852
|
|
73
|
+
connectonion/network/host.py,sha256=Xl6aaQFx4NLapPoTrCkMODGx6kxLmsmWo1zIbjuljRE,20399
|
|
71
74
|
connectonion/network/relay.py,sha256=a8wj4UZDZpGEhvpyDuRjZJg1S1VzxqiPioQRLq1EAao,7160
|
|
72
75
|
connectonion/network/trust.py,sha256=yGnmFq5QVxCqdXgpQD7PPZPrYj3_nhRx0iorVD8Htjg,6698
|
|
73
76
|
connectonion/network/trust_agents.py,sha256=XDedEhxGRfu9KeYhX-Z1a5tRA-2Zbwz4ztnlA2Lnaf0,2968
|
|
74
77
|
connectonion/network/trust_functions.py,sha256=jRgfPm5z8oy9x8IYJd20UUMCz7cc1Pd7zyqQK5SDdLc,3564
|
|
78
|
+
connectonion/network/static/docs.html,sha256=rSffkCAy2fA512XcMC6z1wyZH9iYeAsSP213G3l1OOk,31085
|
|
75
79
|
connectonion/prompt_files/__init__.py,sha256=5o5xg0VzZt4e0O4JKh7AO7gbflkw78oNM37yC3VqFRU,56
|
|
76
80
|
connectonion/prompt_files/analyze_contact.md,sha256=ZTCUhO8mM1mbYswfz4zjBxlv4r0DICLoYm0YaQBpiXk,2110
|
|
77
81
|
connectonion/prompt_files/eval_expected.md,sha256=ZnVmyF9S9m49UzTKCMajJhfGaxUhUkDxDm75xDYNLPs,435
|
|
78
82
|
connectonion/prompt_files/react_evaluate.md,sha256=1qzq_ShlhWUQnh8PkdMl9IpHSwR67FguNc8vaLxzSkA,404
|
|
79
83
|
connectonion/prompt_files/react_plan.md,sha256=QwEDnHsCs1A9AGTyuA1puLRHT0MkUrNEAbVhF3yWNEc,492
|
|
80
84
|
connectonion/prompt_files/reflect.md,sha256=aMaWNbHCyxX3AtqyUrrJurAG6w45lcSo05RrAv6UiIM,627
|
|
81
|
-
connectonion/static/docs.html,sha256=rSffkCAy2fA512XcMC6z1wyZH9iYeAsSP213G3l1OOk,31085
|
|
82
85
|
connectonion/tui/__init__.py,sha256=q8N5Z5lfTmRx-g_m6KfEv7IHOMkChz2PquUGP77vwlg,2711
|
|
83
86
|
connectonion/tui/chat.py,sha256=0vIxfPcf7hWw3pi1mO6AuF4sIEwxKi-Mw25HGnotX80,20941
|
|
84
87
|
connectonion/tui/divider.py,sha256=pKOj7Y3lPwB4TxoWs9J7Pi1axyzMELuw5TIARLyud0o,1018
|
|
@@ -92,13 +95,14 @@ connectonion/tui/providers.py,sha256=czD130jKA9FYUCkLp3eSZ4o7jF7-58uAvCC_9COzApI
|
|
|
92
95
|
connectonion/tui/status_bar.py,sha256=DVqDdc6fr9yHDIDGgLGIuR_4yaPzKPM-kAUJ_dXcNAk,4774
|
|
93
96
|
connectonion/useful_events_handlers/__init__.py,sha256=EMgvTx-MtvMZXtCpyTJkJpSSKJNcTqRKhLmQ8qDoobk,386
|
|
94
97
|
connectonion/useful_events_handlers/reflect.py,sha256=5ZRkc5t-E9KgU02bp8GsquoTAkYG0x71ygARlxr1-fc,3418
|
|
95
|
-
connectonion/useful_plugins/__init__.py,sha256=
|
|
98
|
+
connectonion/useful_plugins/__init__.py,sha256=nqG7B4OtqkTVGOFAF36-K52c_0FzcofHrqsLc9DyXxM,1355
|
|
96
99
|
connectonion/useful_plugins/calendar_plugin.py,sha256=PoQoOfLcprDDBRrt1Ykzlh2RDiOofIyL7tO-hERkYV8,6004
|
|
97
100
|
connectonion/useful_plugins/eval.py,sha256=E0bdzVHRQOnJFcF1jxbvPr0AtEcXux7h9ao5uawsXHQ,4940
|
|
98
101
|
connectonion/useful_plugins/gmail_plugin.py,sha256=xuPR13lnXxn-12zncHPoMNApYJ_zxuoehuavvdGvjvE,5715
|
|
99
102
|
connectonion/useful_plugins/image_result_formatter.py,sha256=d6ME-bVRiSbXoTvIAnwqMyDkJ-xVExOBqbdG9N4ZASE,5342
|
|
100
103
|
connectonion/useful_plugins/re_act.py,sha256=aWBj3lr2d6It3XYAOQp0LbQneM9CPXLxQHW16MgJNKM,3011
|
|
101
104
|
connectonion/useful_plugins/shell_approval.py,sha256=mNPyLcdz8F5Hek77ABGYCdpiefRziyFwc_L8TFVcyoo,6324
|
|
105
|
+
connectonion/useful_plugins/ui_stream.py,sha256=YeS7a0YowUcnhoiffw6uSPlJS7ne5Vkwr_Bbj_6mM_A,4410
|
|
102
106
|
connectonion/useful_tools/__init__.py,sha256=ZG7DVNNSoY7lvyREXleRzqpp53OjttMSiNMS6uA3F4M,1743
|
|
103
107
|
connectonion/useful_tools/diff_writer.py,sha256=ZhuSmTvJRd7RmavCRN9Xp-8Q07mBAgV9_HufWd-i9Lc,6796
|
|
104
108
|
connectonion/useful_tools/get_emails.py,sha256=cechjvRkPH2K-mjoZEnvo1Z6uD1YsVNYQSu56elERm0,6300
|
|
@@ -113,7 +117,7 @@ connectonion/useful_tools/slash_command.py,sha256=VKl7SKLyaxHcHwfE8rgzBCQ2-KQtL0
|
|
|
113
117
|
connectonion/useful_tools/terminal.py,sha256=PtzGHN6vnWROmssi37YOZ5U-d0Z7Fe79bocoKAngoxg,9330
|
|
114
118
|
connectonion/useful_tools/todo_list.py,sha256=wVEt4kt-MczIcP3xvKelfshGHZ6EATpDFzQx0zov8cw,7483
|
|
115
119
|
connectonion/useful_tools/web_fetch.py,sha256=CysJLOdDIkbMVJ12Dj3WgsytLu1-o2wrwNopqA_S1jk,7253
|
|
116
|
-
connectonion-0.6.
|
|
117
|
-
connectonion-0.6.
|
|
118
|
-
connectonion-0.6.
|
|
119
|
-
connectonion-0.6.
|
|
120
|
+
connectonion-0.6.1.dist-info/METADATA,sha256=UfECn-oR2IqKWVKkgNwbOXKQlt_SymSu7O7pkMoNIR0,21932
|
|
121
|
+
connectonion-0.6.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
122
|
+
connectonion-0.6.1.dist-info/entry_points.txt,sha256=XDB-kVN7Qgy4DmYTkjQB_O6hZeUND-SqmZbdoQPn6WA,90
|
|
123
|
+
connectonion-0.6.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|