connectonion 0.5.8__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 +78 -0
- connectonion/address.py +320 -0
- connectonion/agent.py +450 -0
- connectonion/announce.py +84 -0
- connectonion/asgi.py +287 -0
- connectonion/auto_debug_exception.py +181 -0
- connectonion/cli/__init__.py +3 -0
- connectonion/cli/browser_agent/__init__.py +5 -0
- connectonion/cli/browser_agent/browser.py +243 -0
- connectonion/cli/browser_agent/prompt.md +107 -0
- connectonion/cli/commands/__init__.py +1 -0
- connectonion/cli/commands/auth_commands.py +527 -0
- connectonion/cli/commands/browser_commands.py +27 -0
- connectonion/cli/commands/create.py +511 -0
- connectonion/cli/commands/deploy_commands.py +220 -0
- connectonion/cli/commands/doctor_commands.py +173 -0
- connectonion/cli/commands/init.py +469 -0
- connectonion/cli/commands/project_cmd_lib.py +828 -0
- connectonion/cli/commands/reset_commands.py +149 -0
- connectonion/cli/commands/status_commands.py +168 -0
- connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
- connectonion/cli/docs/connectonion.md +1256 -0
- connectonion/cli/docs.md +123 -0
- connectonion/cli/main.py +148 -0
- connectonion/cli/templates/meta-agent/README.md +287 -0
- connectonion/cli/templates/meta-agent/agent.py +196 -0
- connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
- connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
- connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
- connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
- connectonion/cli/templates/minimal/README.md +56 -0
- connectonion/cli/templates/minimal/agent.py +40 -0
- connectonion/cli/templates/playwright/README.md +118 -0
- connectonion/cli/templates/playwright/agent.py +336 -0
- connectonion/cli/templates/playwright/prompt.md +102 -0
- connectonion/cli/templates/playwright/requirements.txt +3 -0
- connectonion/cli/templates/web-research/agent.py +122 -0
- connectonion/connect.py +128 -0
- connectonion/console.py +539 -0
- connectonion/debug_agent/__init__.py +13 -0
- connectonion/debug_agent/agent.py +45 -0
- connectonion/debug_agent/prompts/debug_assistant.md +72 -0
- connectonion/debug_agent/runtime_inspector.py +406 -0
- connectonion/debug_explainer/__init__.py +10 -0
- connectonion/debug_explainer/explain_agent.py +114 -0
- connectonion/debug_explainer/explain_context.py +263 -0
- connectonion/debug_explainer/explainer_prompt.md +29 -0
- connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
- connectonion/debugger_ui.py +1039 -0
- connectonion/decorators.py +208 -0
- connectonion/events.py +248 -0
- connectonion/execution_analyzer/__init__.py +9 -0
- connectonion/execution_analyzer/execution_analysis.py +93 -0
- connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
- connectonion/host.py +579 -0
- connectonion/interactive_debugger.py +342 -0
- connectonion/llm.py +801 -0
- connectonion/llm_do.py +307 -0
- connectonion/logger.py +300 -0
- connectonion/prompt_files/__init__.py +1 -0
- connectonion/prompt_files/analyze_contact.md +62 -0
- connectonion/prompt_files/eval_expected.md +12 -0
- connectonion/prompt_files/react_evaluate.md +11 -0
- connectonion/prompt_files/react_plan.md +16 -0
- connectonion/prompt_files/reflect.md +22 -0
- connectonion/prompts.py +144 -0
- connectonion/relay.py +200 -0
- connectonion/static/docs.html +688 -0
- connectonion/tool_executor.py +279 -0
- connectonion/tool_factory.py +186 -0
- connectonion/tool_registry.py +105 -0
- connectonion/trust.py +166 -0
- connectonion/trust_agents.py +71 -0
- connectonion/trust_functions.py +88 -0
- connectonion/tui/__init__.py +57 -0
- connectonion/tui/divider.py +39 -0
- connectonion/tui/dropdown.py +251 -0
- connectonion/tui/footer.py +31 -0
- connectonion/tui/fuzzy.py +56 -0
- connectonion/tui/input.py +278 -0
- connectonion/tui/keys.py +35 -0
- connectonion/tui/pick.py +130 -0
- connectonion/tui/providers.py +155 -0
- connectonion/tui/status_bar.py +163 -0
- connectonion/usage.py +161 -0
- connectonion/useful_events_handlers/__init__.py +16 -0
- connectonion/useful_events_handlers/reflect.py +116 -0
- connectonion/useful_plugins/__init__.py +20 -0
- connectonion/useful_plugins/calendar_plugin.py +163 -0
- connectonion/useful_plugins/eval.py +139 -0
- connectonion/useful_plugins/gmail_plugin.py +162 -0
- connectonion/useful_plugins/image_result_formatter.py +127 -0
- connectonion/useful_plugins/re_act.py +78 -0
- connectonion/useful_plugins/shell_approval.py +159 -0
- connectonion/useful_tools/__init__.py +44 -0
- connectonion/useful_tools/diff_writer.py +192 -0
- connectonion/useful_tools/get_emails.py +183 -0
- connectonion/useful_tools/gmail.py +1596 -0
- connectonion/useful_tools/google_calendar.py +613 -0
- connectonion/useful_tools/memory.py +380 -0
- connectonion/useful_tools/microsoft_calendar.py +604 -0
- connectonion/useful_tools/outlook.py +488 -0
- connectonion/useful_tools/send_email.py +205 -0
- connectonion/useful_tools/shell.py +97 -0
- connectonion/useful_tools/slash_command.py +201 -0
- connectonion/useful_tools/terminal.py +285 -0
- connectonion/useful_tools/todo_list.py +241 -0
- connectonion/useful_tools/web_fetch.py +216 -0
- connectonion/xray.py +467 -0
- connectonion-0.5.8.dist-info/METADATA +741 -0
- connectonion-0.5.8.dist-info/RECORD +113 -0
- connectonion-0.5.8.dist-info/WHEEL +4 -0
- connectonion-0.5.8.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
You are evaluating whether a task is complete.
|
|
2
|
+
|
|
3
|
+
Given the user's original request and what was done, determine:
|
|
4
|
+
1. Is the task truly complete?
|
|
5
|
+
2. If not, what's still missing?
|
|
6
|
+
|
|
7
|
+
Be honest and critical. Don't say "complete" if important parts are missing.
|
|
8
|
+
|
|
9
|
+
Respond in ONE sentence:
|
|
10
|
+
- If complete: "Task complete: [brief summary of what was achieved]"
|
|
11
|
+
- If incomplete: "Not done yet: [what's still needed]"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ReAct Planning
|
|
2
|
+
|
|
3
|
+
You are a planning assistant. When a user gives a task, think about it briefly.
|
|
4
|
+
|
|
5
|
+
## Guidelines
|
|
6
|
+
|
|
7
|
+
- Keep it SHORT: 1-2 sentences max
|
|
8
|
+
- Focus on: What needs to be done and which tools to use
|
|
9
|
+
- Be practical and actionable
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
- "Need to check emails first, then reply to the urgent ones."
|
|
14
|
+
- "Will search for the file, then read its contents."
|
|
15
|
+
- "Should fetch the API data and analyze the response."
|
|
16
|
+
- "Let me check the database for user info, then format the report."
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Reflect
|
|
2
|
+
|
|
3
|
+
You are a reflection assistant. After each tool execution, provide a brief one-sentence reflection.
|
|
4
|
+
|
|
5
|
+
## Guidelines
|
|
6
|
+
|
|
7
|
+
- Keep it SHORT: One sentence only
|
|
8
|
+
- Focus on: What happened + What's next
|
|
9
|
+
- Be practical and actionable
|
|
10
|
+
- No verbose explanations
|
|
11
|
+
|
|
12
|
+
## Examples
|
|
13
|
+
|
|
14
|
+
Success cases:
|
|
15
|
+
- "Got 5 emails, let's check the first one for details."
|
|
16
|
+
- "Contact updated, now searching for related conversations."
|
|
17
|
+
- "File created successfully, running tests next."
|
|
18
|
+
|
|
19
|
+
Error cases:
|
|
20
|
+
- "Permission denied, need to check file ownership."
|
|
21
|
+
- "API rate limited, waiting before retry."
|
|
22
|
+
- "Invalid email format, asking user for correct address."
|
connectonion/prompts.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Load and validate system prompts from files or strings with intelligent path detection
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [os, warnings, pathlib, typing] | imported by [agent.py] | no dedicated tests found
|
|
5
|
+
Data flow: receives system_prompt: Union[str, Path, None] from Agent.__init__ → checks if None (returns DEFAULT_PROMPT) → checks if Path object (reads file) → checks if str exists as file (reads) → warns if looks like file but doesn't exist → returns literal string
|
|
6
|
+
State/Effects: reads text files if path provided | emits UserWarning if path looks like file but doesn't exist | no writes or global state
|
|
7
|
+
Integration: exposes load_system_prompt(prompt), DEFAULT_PROMPT constant | used by Agent to load system prompts from various sources | supports .md, .txt, .prompt file extensions | Path objects enforce file must exist
|
|
8
|
+
Performance: file I/O only when path provided | heuristic checks (file extension, path separators) are fast string operations
|
|
9
|
+
Errors: raises FileNotFoundError if Path doesn't exist | raises ValueError if Path is not a file or file is empty | raises ValueError if file not UTF-8 | warns (doesn't fail) for str that looks like missing file
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import warnings
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Union
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_PROMPT = "You are a helpful assistant that can use tools to complete tasks."
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _looks_like_file_path(text: str) -> bool:
|
|
22
|
+
"""Check if a string looks like a file path rather than prompt text."""
|
|
23
|
+
# Check for file extensions
|
|
24
|
+
has_file_extension = '.' in text and text.split('.')[-1] in ['md', 'txt', 'prompt']
|
|
25
|
+
# Check for path separators
|
|
26
|
+
has_path_separator = '/' in text or '\\' in text
|
|
27
|
+
|
|
28
|
+
return has_file_extension or has_path_separator
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _warn_if_missing_file(prompt: str) -> None:
|
|
32
|
+
"""Warn user if prompt looks like a file path but doesn't exist."""
|
|
33
|
+
if _looks_like_file_path(prompt) and not os.path.exists(prompt):
|
|
34
|
+
abs_path = os.path.abspath(prompt)
|
|
35
|
+
cwd = os.getcwd()
|
|
36
|
+
|
|
37
|
+
# Suggest better approach
|
|
38
|
+
suggestion = ""
|
|
39
|
+
if '/' in prompt or '\\' in prompt:
|
|
40
|
+
# Has path separators, suggest Path object
|
|
41
|
+
suggestion = f"\n Tip: Use Path object for explicit file loading: Path('{prompt}')"
|
|
42
|
+
else:
|
|
43
|
+
# Just filename, suggest correct directory or Path
|
|
44
|
+
suggestion = (f"\n Tip: Either run from the correct directory, or use absolute path:\n"
|
|
45
|
+
f" Path(__file__).parent / '{prompt}'")
|
|
46
|
+
|
|
47
|
+
warnings.warn(
|
|
48
|
+
f"'{prompt}' looks like a file path but doesn't exist.\n"
|
|
49
|
+
f" Looked in: {abs_path}\n"
|
|
50
|
+
f" Current directory: {cwd}\n"
|
|
51
|
+
f" Treating as literal prompt text."
|
|
52
|
+
f"{suggestion}",
|
|
53
|
+
UserWarning,
|
|
54
|
+
stacklevel=3
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_system_prompt(prompt: Union[str, Path, None]) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Load system prompt from various sources.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
prompt: Can be:
|
|
64
|
+
- None: Returns default prompt
|
|
65
|
+
- str: Either a file path (if file exists) or literal prompt text
|
|
66
|
+
- Path: Path object to a text file
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: The loaded system prompt
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
>>> load_system_prompt(None)
|
|
73
|
+
'You are a helpful assistant that can use tools to complete tasks.'
|
|
74
|
+
|
|
75
|
+
>>> load_system_prompt("You are a helpful assistant")
|
|
76
|
+
'You are a helpful assistant'
|
|
77
|
+
|
|
78
|
+
>>> load_system_prompt("prompts/assistant.md") # If file exists
|
|
79
|
+
# Returns content from the file
|
|
80
|
+
|
|
81
|
+
>>> load_system_prompt(Path("prompts/assistant")) # Any text file
|
|
82
|
+
# Returns content from the file
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
FileNotFoundError: If a Path object points to non-existent file
|
|
86
|
+
ValueError: If file is empty or not valid UTF-8 text
|
|
87
|
+
"""
|
|
88
|
+
if prompt is None:
|
|
89
|
+
return DEFAULT_PROMPT
|
|
90
|
+
|
|
91
|
+
if isinstance(prompt, Path):
|
|
92
|
+
# Explicit Path object - must exist
|
|
93
|
+
if not prompt.exists():
|
|
94
|
+
raise FileNotFoundError(f"Prompt file not found: {prompt}")
|
|
95
|
+
if not prompt.is_file():
|
|
96
|
+
raise ValueError(f"Path is not a file: {prompt}")
|
|
97
|
+
return _read_text_file(prompt)
|
|
98
|
+
|
|
99
|
+
if isinstance(prompt, str):
|
|
100
|
+
# Check if it's an existing file
|
|
101
|
+
if os.path.exists(prompt) and os.path.isfile(prompt):
|
|
102
|
+
return _read_text_file(Path(prompt))
|
|
103
|
+
|
|
104
|
+
# Warn if it looks like a missing file
|
|
105
|
+
_warn_if_missing_file(prompt)
|
|
106
|
+
|
|
107
|
+
# Treat as literal prompt text
|
|
108
|
+
return prompt
|
|
109
|
+
|
|
110
|
+
raise TypeError(f"Invalid prompt type: {type(prompt).__name__}. Expected str, Path, or None.")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _read_text_file(path: Path) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Read content from a text file.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
path: Path to the text file
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
str: File content
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ValueError: If file is empty or not valid UTF-8
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
content = path.read_text(encoding='utf-8').strip()
|
|
128
|
+
if not content:
|
|
129
|
+
raise ValueError(f"Prompt file '{path}' is empty. Please add content or use a different file.")
|
|
130
|
+
return content
|
|
131
|
+
except UnicodeDecodeError:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"File '{path}' is not a valid UTF-8 text file. "
|
|
134
|
+
f"System prompts must be text files."
|
|
135
|
+
)
|
|
136
|
+
except ValueError:
|
|
137
|
+
# Re-raise ValueError (empty file)
|
|
138
|
+
raise
|
|
139
|
+
except PermissionError:
|
|
140
|
+
# Re-raise PermissionError
|
|
141
|
+
raise
|
|
142
|
+
except Exception as e:
|
|
143
|
+
# Catch any other unexpected errors
|
|
144
|
+
raise RuntimeError(f"Error reading prompt file '{path}': {e}")
|
connectonion/relay.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: WebSocket relay client for agent-to-agent communication via central relay server using INPUT/OUTPUT protocol
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [json, asyncio, typing, websockets] | imported by [host.py] | tested by [tests/test_relay.py]
|
|
5
|
+
Data flow: host() with relay_url → connect(relay_url) → WebSocket established → send_announce(ws, announce_msg) → serve_loop() → wait_for_task(ws) receives INPUT message from relay → task_handler(prompt) executes → send OUTPUT response via WebSocket → heartbeat re-announces every 60s
|
|
6
|
+
State/Effects: maintains WebSocket connection to relay | reads incoming JSON messages (INPUT type) | writes outgoing JSON messages (OUTPUT type) | prints status to stdout | asyncio timeout for heartbeat | no file I/O
|
|
7
|
+
Integration: exposes connect(relay_url), send_announce(ws, msg), wait_for_task(ws, timeout), send_response(ws, input_id, result), serve_loop(ws, announce_msg, task_handler, heartbeat_interval) | used by host() with relay_url to make agent discoverable on relay network | task_handler is async function (prompt: str) -> str | Protocol: INPUT/OUTPUT messages (not TASK/RESPONSE)
|
|
8
|
+
Performance: async/await non-blocking I/O | heartbeat_interval=60s default (configurable) | timeout-based heartbeat scheduling | WebSocket maintains persistent connection
|
|
9
|
+
Errors: let it crash - ImportError if websockets missing | asyncio.TimeoutError used for heartbeat timing | websockets.ConnectionClosed exits serve loop gracefully
|
|
10
|
+
|
|
11
|
+
WebSocket relay client functions.
|
|
12
|
+
|
|
13
|
+
Simple async functions for connecting to relay and exchanging messages using INPUT/OUTPUT protocol.
|
|
14
|
+
No classes needed - just stateless functions operating on websocket connections.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import asyncio
|
|
19
|
+
from typing import Dict, Any
|
|
20
|
+
import websockets
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def connect(relay_url: str = "wss://oo.openonion.ai/ws/announce"):
|
|
24
|
+
"""
|
|
25
|
+
Connect to relay WebSocket endpoint.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
relay_url: WebSocket URL for relay (default: production relay)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
WebSocket connection object
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> ws = await connect()
|
|
35
|
+
>>> # Now use ws for sending/receiving
|
|
36
|
+
"""
|
|
37
|
+
return await websockets.connect(relay_url)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def send_announce(websocket, announce_message: Dict[str, Any]):
|
|
41
|
+
"""
|
|
42
|
+
Send ANNOUNCE message through WebSocket.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
websocket: WebSocket connection from connect()
|
|
46
|
+
announce_message: Dict from create_announce_message()
|
|
47
|
+
|
|
48
|
+
Note:
|
|
49
|
+
Server responds with error message only if something went wrong.
|
|
50
|
+
No response = success (per protocol spec)
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> from . import announce, address
|
|
54
|
+
>>> addr = address.load()
|
|
55
|
+
>>> msg = announce.create_announce_message(addr, "My agent", [])
|
|
56
|
+
>>> await send_announce(ws, msg)
|
|
57
|
+
"""
|
|
58
|
+
message_json = json.dumps(announce_message)
|
|
59
|
+
await websocket.send(message_json)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def wait_for_task(websocket, timeout: float = None) -> Dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Wait for next INPUT message from relay.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
websocket: WebSocket connection from connect()
|
|
68
|
+
timeout: Optional timeout in seconds (None = wait forever)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
INPUT message dict:
|
|
72
|
+
{
|
|
73
|
+
"type": "INPUT",
|
|
74
|
+
"input_id": "abc123...",
|
|
75
|
+
"prompt": "Translate hello to Spanish",
|
|
76
|
+
"from_address": "0x..."
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
asyncio.TimeoutError: If timeout expires
|
|
81
|
+
websockets.exceptions.ConnectionClosed: If connection lost
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> task = await wait_for_task(ws)
|
|
85
|
+
>>> print(task["prompt"])
|
|
86
|
+
Translate hello to Spanish
|
|
87
|
+
"""
|
|
88
|
+
if timeout:
|
|
89
|
+
data = await asyncio.wait_for(websocket.recv(), timeout=timeout)
|
|
90
|
+
else:
|
|
91
|
+
data = await websocket.recv()
|
|
92
|
+
|
|
93
|
+
message = json.loads(data)
|
|
94
|
+
return message
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def send_response(
|
|
98
|
+
websocket,
|
|
99
|
+
input_id: str,
|
|
100
|
+
result: str,
|
|
101
|
+
success: bool = True
|
|
102
|
+
):
|
|
103
|
+
"""
|
|
104
|
+
Send output response back to relay.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
websocket: WebSocket connection from connect()
|
|
108
|
+
input_id: ID from INPUT message
|
|
109
|
+
result: Agent's response/output
|
|
110
|
+
success: Whether task succeeded (default True)
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
>>> task = await wait_for_task(ws)
|
|
114
|
+
>>> result = agent.input(task["prompt"])
|
|
115
|
+
>>> await send_response(ws, task["input_id"], result)
|
|
116
|
+
"""
|
|
117
|
+
response_message = {
|
|
118
|
+
"type": "OUTPUT",
|
|
119
|
+
"input_id": input_id,
|
|
120
|
+
"result": result,
|
|
121
|
+
"success": success
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
message_json = json.dumps(response_message)
|
|
125
|
+
await websocket.send(message_json)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def serve_loop(
|
|
129
|
+
websocket,
|
|
130
|
+
announce_message: Dict[str, Any],
|
|
131
|
+
task_handler,
|
|
132
|
+
heartbeat_interval: int = 60
|
|
133
|
+
):
|
|
134
|
+
"""
|
|
135
|
+
Main serving loop for agent.
|
|
136
|
+
|
|
137
|
+
This handles:
|
|
138
|
+
- Initial ANNOUNCE
|
|
139
|
+
- Periodic heartbeat ANNOUNCE (every 60s)
|
|
140
|
+
- Receiving and processing TASK messages
|
|
141
|
+
- Sending responses
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
websocket: WebSocket connection from connect()
|
|
145
|
+
announce_message: ANNOUNCE message dict (will be re-sent for heartbeat)
|
|
146
|
+
task_handler: Async function that takes (prompt: str) -> str
|
|
147
|
+
heartbeat_interval: Seconds between heartbeat ANNOUNCEs (default 60)
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
>>> async def handler(prompt):
|
|
151
|
+
... return agent.input(prompt)
|
|
152
|
+
>>> await serve_loop(ws, announce_msg, handler)
|
|
153
|
+
"""
|
|
154
|
+
# Send initial ANNOUNCE
|
|
155
|
+
await send_announce(websocket, announce_message)
|
|
156
|
+
print(f"✓ Announced to relay: {announce_message['address'][:12]}...")
|
|
157
|
+
|
|
158
|
+
# Track last heartbeat time
|
|
159
|
+
last_heartbeat = asyncio.get_event_loop().time()
|
|
160
|
+
|
|
161
|
+
# Main loop
|
|
162
|
+
while True:
|
|
163
|
+
try:
|
|
164
|
+
# Wait for message with timeout to allow heartbeat
|
|
165
|
+
task = await wait_for_task(websocket, timeout=heartbeat_interval)
|
|
166
|
+
|
|
167
|
+
# Handle INPUT message
|
|
168
|
+
if task.get("type") == "INPUT":
|
|
169
|
+
print(f"→ Received input: {task['input_id'][:8]}...")
|
|
170
|
+
|
|
171
|
+
# Process with handler
|
|
172
|
+
result = await task_handler(task["prompt"])
|
|
173
|
+
|
|
174
|
+
# Send OUTPUT response
|
|
175
|
+
output_message = {
|
|
176
|
+
"type": "OUTPUT",
|
|
177
|
+
"input_id": task["input_id"],
|
|
178
|
+
"result": result
|
|
179
|
+
}
|
|
180
|
+
await websocket.send(json.dumps(output_message))
|
|
181
|
+
print(f"✓ Sent output: {task['input_id'][:8]}...")
|
|
182
|
+
|
|
183
|
+
elif task.get("type") == "ERROR":
|
|
184
|
+
print(f"✗ Error from relay: {task.get('error')}")
|
|
185
|
+
|
|
186
|
+
except asyncio.TimeoutError:
|
|
187
|
+
# Time for heartbeat ANNOUNCE
|
|
188
|
+
# Update timestamp in message
|
|
189
|
+
announce_message["timestamp"] = int(asyncio.get_event_loop().time())
|
|
190
|
+
|
|
191
|
+
# Need to re-sign with new timestamp
|
|
192
|
+
# For now, just send without updating signature
|
|
193
|
+
# TODO: Re-sign message with new timestamp
|
|
194
|
+
await send_announce(websocket, announce_message)
|
|
195
|
+
print("♥ Sent heartbeat")
|
|
196
|
+
last_heartbeat = asyncio.get_event_loop().time()
|
|
197
|
+
|
|
198
|
+
except websockets.exceptions.ConnectionClosed:
|
|
199
|
+
print("✗ Connection to relay closed")
|
|
200
|
+
break
|