connectonion 0.5.8__py3-none-any.whl → 0.5.10__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 -1
- connectonion/cli/commands/copy_commands.py +116 -0
- connectonion/cli/main.py +14 -1
- connectonion/connect.py +178 -34
- connectonion/host.py +31 -14
- connectonion/transcribe.py +245 -0
- {connectonion-0.5.8.dist-info → connectonion-0.5.10.dist-info}/METADATA +1 -1
- {connectonion-0.5.8.dist-info → connectonion-0.5.10.dist-info}/RECORD +10 -8
- {connectonion-0.5.8.dist-info → connectonion-0.5.10.dist-info}/WHEEL +0 -0
- {connectonion-0.5.8.dist-info → connectonion-0.5.10.dist-info}/entry_points.txt +0 -0
connectonion/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""ConnectOnion - A simple agent framework with behavior tracking."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.5.
|
|
3
|
+
__version__ = "0.5.10"
|
|
4
4
|
|
|
5
5
|
# Auto-load .env files for the entire framework
|
|
6
6
|
from dotenv import load_dotenv
|
|
@@ -15,6 +15,7 @@ from .tool_factory import create_tool_from_function
|
|
|
15
15
|
from .llm import LLM
|
|
16
16
|
from .logger import Logger
|
|
17
17
|
from .llm_do import llm_do
|
|
18
|
+
from .transcribe import transcribe
|
|
18
19
|
from .prompts import load_system_prompt
|
|
19
20
|
from .xray import xray
|
|
20
21
|
from .decorators import replay, xray_replay
|
|
@@ -40,6 +41,7 @@ __all__ = [
|
|
|
40
41
|
"Logger",
|
|
41
42
|
"create_tool_from_function",
|
|
42
43
|
"llm_do",
|
|
44
|
+
"transcribe",
|
|
43
45
|
"load_system_prompt",
|
|
44
46
|
"xray",
|
|
45
47
|
"replay",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI command to copy built-in tools and plugins to user's project for customization
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [shutil, pathlib, typing, rich] | imported by [cli/main.py via handle_copy()]
|
|
5
|
+
Data flow: user runs `co copy <name>` → looks up name in TOOLS/PLUGINS registry → finds source via module.__file__ → copies to ./tools/ or ./plugins/
|
|
6
|
+
State/Effects: creates tools/ or plugins/ directory if needed | copies .py files from installed package to user's project
|
|
7
|
+
Integration: exposes handle_copy() for CLI | uses Python import system to find installed package location (cross-platform)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, List
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
# Registry of copyable tools
|
|
19
|
+
TOOLS = {
|
|
20
|
+
"gmail": "gmail.py",
|
|
21
|
+
"outlook": "outlook.py",
|
|
22
|
+
"google_calendar": "google_calendar.py",
|
|
23
|
+
"microsoft_calendar": "microsoft_calendar.py",
|
|
24
|
+
"memory": "memory.py",
|
|
25
|
+
"web_fetch": "web_fetch.py",
|
|
26
|
+
"shell": "shell.py",
|
|
27
|
+
"diff_writer": "diff_writer.py",
|
|
28
|
+
"todo_list": "todo_list.py",
|
|
29
|
+
"slash_command": "slash_command.py",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Registry of copyable plugins
|
|
33
|
+
PLUGINS = {
|
|
34
|
+
"re_act": "re_act.py",
|
|
35
|
+
"eval": "eval.py",
|
|
36
|
+
"image_result_formatter": "image_result_formatter.py",
|
|
37
|
+
"shell_approval": "shell_approval.py",
|
|
38
|
+
"gmail_plugin": "gmail_plugin.py",
|
|
39
|
+
"calendar_plugin": "calendar_plugin.py",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def handle_copy(
|
|
44
|
+
names: List[str],
|
|
45
|
+
list_all: bool = False,
|
|
46
|
+
path: Optional[str] = None,
|
|
47
|
+
force: bool = False
|
|
48
|
+
):
|
|
49
|
+
"""Copy built-in tools and plugins to user's project."""
|
|
50
|
+
|
|
51
|
+
# Show list if requested or no names provided
|
|
52
|
+
if list_all or not names:
|
|
53
|
+
show_available_items()
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# Get source directories using import system (works for installed packages)
|
|
57
|
+
import connectonion.useful_tools as tools_module
|
|
58
|
+
import connectonion.useful_plugins as plugins_module
|
|
59
|
+
|
|
60
|
+
useful_tools_dir = Path(tools_module.__file__).parent
|
|
61
|
+
useful_plugins_dir = Path(plugins_module.__file__).parent
|
|
62
|
+
|
|
63
|
+
current_dir = Path.cwd()
|
|
64
|
+
|
|
65
|
+
for name in names:
|
|
66
|
+
name_lower = name.lower()
|
|
67
|
+
|
|
68
|
+
# Check if it's a tool
|
|
69
|
+
if name_lower in TOOLS:
|
|
70
|
+
source = useful_tools_dir / TOOLS[name_lower]
|
|
71
|
+
dest_dir = Path(path) if path else current_dir / "tools"
|
|
72
|
+
copy_file(source, dest_dir, force)
|
|
73
|
+
|
|
74
|
+
# Check if it's a plugin
|
|
75
|
+
elif name_lower in PLUGINS:
|
|
76
|
+
source = useful_plugins_dir / PLUGINS[name_lower]
|
|
77
|
+
dest_dir = Path(path) if path else current_dir / "plugins"
|
|
78
|
+
copy_file(source, dest_dir, force)
|
|
79
|
+
|
|
80
|
+
else:
|
|
81
|
+
console.print(f"[red]Unknown: {name}[/red]")
|
|
82
|
+
console.print("Use [cyan]co copy --list[/cyan] to see available items")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def copy_file(source: Path, dest_dir: Path, force: bool):
|
|
86
|
+
"""Copy a single file to destination."""
|
|
87
|
+
if not source.exists():
|
|
88
|
+
console.print(f"[red]Source not found: {source}[/red]")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
dest = dest_dir / source.name
|
|
93
|
+
|
|
94
|
+
if dest.exists() and not force:
|
|
95
|
+
console.print(f"[yellow]Skipped: {dest} (exists, use --force)[/yellow]")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
shutil.copy2(source, dest)
|
|
99
|
+
console.print(f"[green]✓ Copied: {dest}[/green]")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def show_available_items():
|
|
103
|
+
"""Display available tools and plugins."""
|
|
104
|
+
table = Table(title="Available Items to Copy")
|
|
105
|
+
table.add_column("Name", style="cyan")
|
|
106
|
+
table.add_column("Type", style="green")
|
|
107
|
+
table.add_column("File")
|
|
108
|
+
|
|
109
|
+
for name, file in sorted(TOOLS.items()):
|
|
110
|
+
table.add_row(name, "tool", file)
|
|
111
|
+
|
|
112
|
+
for name, file in sorted(PLUGINS.items()):
|
|
113
|
+
table.add_row(name, "plugin", file)
|
|
114
|
+
|
|
115
|
+
console.print(table)
|
|
116
|
+
console.print("\n[dim]Usage: co copy <name> [--path ./custom/][/dim]")
|
connectonion/cli/main.py
CHANGED
|
@@ -11,7 +11,7 @@ LLM-Note:
|
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
13
|
from rich.console import Console
|
|
14
|
-
from typing import Optional
|
|
14
|
+
from typing import Optional, List
|
|
15
15
|
|
|
16
16
|
from .. import __version__
|
|
17
17
|
|
|
@@ -54,6 +54,7 @@ def _show_help():
|
|
|
54
54
|
console.print("[bold]Commands:[/bold]")
|
|
55
55
|
console.print(" [green]create[/green] <name> Create new project")
|
|
56
56
|
console.print(" [green]init[/green] Initialize in current directory")
|
|
57
|
+
console.print(" [green]copy[/green] <name> Copy tool/plugin source to project")
|
|
57
58
|
console.print(" [green]deploy[/green] Deploy to ConnectOnion Cloud")
|
|
58
59
|
console.print(" [green]auth[/green] Authenticate for managed keys")
|
|
59
60
|
console.print(" [green]status[/green] Check account balance")
|
|
@@ -139,6 +140,18 @@ def browser(command: str = typer.Argument(..., help="Browser command")):
|
|
|
139
140
|
handle_browser(command)
|
|
140
141
|
|
|
141
142
|
|
|
143
|
+
@app.command()
|
|
144
|
+
def copy(
|
|
145
|
+
names: List[str] = typer.Argument(None, help="Tool or plugin names to copy"),
|
|
146
|
+
list_all: bool = typer.Option(False, "--list", "-l", help="List available items"),
|
|
147
|
+
path: Optional[str] = typer.Option(None, "--path", "-p", help="Custom destination path"),
|
|
148
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
|
149
|
+
):
|
|
150
|
+
"""Copy built-in tools/plugins to customize."""
|
|
151
|
+
from .commands.copy_commands import handle_copy
|
|
152
|
+
handle_copy(names=names or [], list_all=list_all, path=path, force=force)
|
|
153
|
+
|
|
154
|
+
|
|
142
155
|
def cli():
|
|
143
156
|
"""Entry point."""
|
|
144
157
|
app()
|
connectonion/connect.py
CHANGED
|
@@ -1,40 +1,70 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Purpose: Client interface for connecting to remote agents via relay network
|
|
2
|
+
Purpose: Client interface for connecting to remote agents via HTTP or relay network
|
|
3
3
|
LLM-Note:
|
|
4
|
-
Dependencies: imports from [asyncio, json, uuid, websockets] | imported by [__init__.py, tests/test_connect.py, examples/] | tested by [tests/test_connect.py
|
|
5
|
-
Data flow:
|
|
6
|
-
State/Effects:
|
|
7
|
-
Integration: exposes connect(address, relay_url), RemoteAgent class with .input(
|
|
8
|
-
Performance:
|
|
9
|
-
Errors: raises ImportError if websockets not installed | raises ConnectionError for ERROR responses from relay | raises ConnectionError for unexpected response types | asyncio.TimeoutError if no response within timeout | WebSocket connection errors bubble up
|
|
4
|
+
Dependencies: imports from [asyncio, json, uuid, time, aiohttp, websockets, address] | imported by [__init__.py, tests/test_connect.py, examples/] | tested by [tests/test_connect.py]
|
|
5
|
+
Data flow: connect(address, keys) → RemoteAgent → input() → discover endpoints → try HTTP first → fallback to relay → return result
|
|
6
|
+
State/Effects: caches discovered endpoint for reuse | optional signing with keys parameter
|
|
7
|
+
Integration: exposes connect(address, keys, relay_url), RemoteAgent class with .input(), .input_async()
|
|
8
|
+
Performance: discovery cached per RemoteAgent instance | HTTPS tried first (direct), relay as fallback
|
|
10
9
|
|
|
11
10
|
Connect to remote agents on the network.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
Smart discovery: tries HTTP endpoints first, falls back to relay.
|
|
13
|
+
Always signs requests when keys are provided.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
17
|
import json
|
|
18
|
+
import time
|
|
18
19
|
import uuid
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from . import address as addr
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
class RemoteAgent:
|
|
22
26
|
"""
|
|
23
27
|
Interface to a remote agent.
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
Supports:
|
|
30
|
+
- Discovery via relay API
|
|
31
|
+
- Direct HTTP POST to agent /input endpoint
|
|
32
|
+
- WebSocket relay fallback
|
|
33
|
+
- Signed requests when keys provided
|
|
34
|
+
- Multi-turn conversations via session management
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
# Standard Python scripts
|
|
38
|
+
agent = connect("0x...")
|
|
39
|
+
result = agent.input("Hello")
|
|
40
|
+
|
|
41
|
+
# Jupyter notebooks or async code
|
|
42
|
+
agent = connect("0x...")
|
|
43
|
+
result = await agent.input_async("Hello")
|
|
26
44
|
"""
|
|
27
45
|
|
|
28
|
-
def __init__(
|
|
29
|
-
self
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
agent_address: str,
|
|
49
|
+
*,
|
|
50
|
+
keys: Optional[Dict[str, Any]] = None,
|
|
51
|
+
relay_url: str = "wss://oo.openonion.ai/ws/announce"
|
|
52
|
+
):
|
|
53
|
+
self.address = agent_address
|
|
54
|
+
self._keys = keys
|
|
30
55
|
self._relay_url = relay_url
|
|
56
|
+
self._cached_endpoint: Optional[str] = None
|
|
57
|
+
self._session: Optional[Dict[str, Any]] = None # Multi-turn conversation state
|
|
31
58
|
|
|
32
59
|
def input(self, prompt: str, timeout: float = 30.0) -> str:
|
|
33
60
|
"""
|
|
34
61
|
Send task to remote agent and get response (sync version).
|
|
35
62
|
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
Automatically maintains conversation context across calls.
|
|
64
|
+
|
|
65
|
+
Note:
|
|
66
|
+
This method cannot be used inside an async context (e.g., Jupyter notebooks,
|
|
67
|
+
async functions). Use input_async() instead in those environments.
|
|
38
68
|
|
|
39
69
|
Args:
|
|
40
70
|
prompt: Task/prompt to send
|
|
@@ -43,17 +73,32 @@ class RemoteAgent:
|
|
|
43
73
|
Returns:
|
|
44
74
|
Agent's response string
|
|
45
75
|
|
|
76
|
+
Raises:
|
|
77
|
+
RuntimeError: If called from within a running event loop
|
|
78
|
+
|
|
46
79
|
Example:
|
|
47
80
|
>>> translator = connect("0x3d40...")
|
|
48
81
|
>>> result = translator.input("Translate 'hello' to Spanish")
|
|
82
|
+
>>> # Continue conversation
|
|
83
|
+
>>> result2 = translator.input("Now translate it to French")
|
|
49
84
|
"""
|
|
85
|
+
try:
|
|
86
|
+
asyncio.get_running_loop()
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
"input() cannot be used inside async context (e.g., Jupyter notebooks). "
|
|
89
|
+
"Use 'await agent.input_async()' instead."
|
|
90
|
+
)
|
|
91
|
+
except RuntimeError as e:
|
|
92
|
+
if "input() cannot be used" in str(e):
|
|
93
|
+
raise
|
|
94
|
+
# No running loop - safe to proceed
|
|
50
95
|
return asyncio.run(self._send_task(prompt, timeout))
|
|
51
96
|
|
|
52
97
|
async def input_async(self, prompt: str, timeout: float = 30.0) -> str:
|
|
53
98
|
"""
|
|
54
99
|
Send task to remote agent and get response (async version).
|
|
55
100
|
|
|
56
|
-
|
|
101
|
+
Automatically maintains conversation context across calls.
|
|
57
102
|
|
|
58
103
|
Args:
|
|
59
104
|
prompt: Task/prompt to send
|
|
@@ -61,42 +106,93 @@ class RemoteAgent:
|
|
|
61
106
|
|
|
62
107
|
Returns:
|
|
63
108
|
Agent's response string
|
|
64
|
-
|
|
65
|
-
Example:
|
|
66
|
-
>>> remote = connect("0x3d40...")
|
|
67
|
-
>>> result = await remote.input_async("Translate 'hello' to Spanish")
|
|
68
109
|
"""
|
|
69
110
|
return await self._send_task(prompt, timeout)
|
|
70
111
|
|
|
71
|
-
|
|
72
|
-
"""
|
|
73
|
-
|
|
112
|
+
def reset_conversation(self):
|
|
113
|
+
"""Clear conversation history and start fresh."""
|
|
114
|
+
self._session = None
|
|
74
115
|
|
|
75
|
-
|
|
76
|
-
"""
|
|
116
|
+
def _sign_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
117
|
+
"""Sign a payload if keys are available."""
|
|
118
|
+
if not self._keys:
|
|
119
|
+
return {"prompt": payload.get("prompt", "")}
|
|
120
|
+
|
|
121
|
+
canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
|
|
122
|
+
signature = addr.sign(self._keys, canonical.encode())
|
|
123
|
+
return {
|
|
124
|
+
"payload": payload,
|
|
125
|
+
"from": self._keys["address"],
|
|
126
|
+
"signature": signature.hex()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async def _discover_endpoints(self) -> List[str]:
|
|
130
|
+
"""Query relay API for agent endpoints."""
|
|
131
|
+
import aiohttp
|
|
132
|
+
|
|
133
|
+
# Convert wss://oo.openonion.ai/ws/announce to https://oo.openonion.ai
|
|
134
|
+
base_url = self._relay_url.replace("wss://", "https://").replace("ws://", "http://")
|
|
135
|
+
base_url = base_url.replace("/ws/announce", "")
|
|
136
|
+
|
|
137
|
+
async with aiohttp.ClientSession() as session:
|
|
138
|
+
async with session.get(f"{base_url}/api/relay/agents/{self.address}") as resp:
|
|
139
|
+
if resp.status == 200:
|
|
140
|
+
data = await resp.json()
|
|
141
|
+
if data.get("online"):
|
|
142
|
+
return data.get("endpoints", [])
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
def _create_signed_body(self, prompt: str) -> Dict[str, Any]:
|
|
146
|
+
"""Create signed request body for agent /input endpoint."""
|
|
147
|
+
payload = {"prompt": prompt, "to": self.address, "timestamp": int(time.time())}
|
|
148
|
+
body = self._sign_payload(payload)
|
|
149
|
+
if self._session:
|
|
150
|
+
body["session"] = self._session
|
|
151
|
+
return body
|
|
152
|
+
|
|
153
|
+
async def _send_http(self, endpoint: str, prompt: str, timeout: float) -> str:
|
|
154
|
+
"""Send request via direct HTTP POST to agent /input endpoint."""
|
|
155
|
+
import aiohttp
|
|
156
|
+
|
|
157
|
+
body = self._create_signed_body(prompt)
|
|
158
|
+
|
|
159
|
+
async with aiohttp.ClientSession() as http_session:
|
|
160
|
+
async with http_session.post(
|
|
161
|
+
f"{endpoint}/input",
|
|
162
|
+
json=body,
|
|
163
|
+
timeout=aiohttp.ClientTimeout(total=timeout)
|
|
164
|
+
) as resp:
|
|
165
|
+
data = await resp.json()
|
|
166
|
+
if not resp.ok:
|
|
167
|
+
raise ConnectionError(data.get("error", f"HTTP {resp.status}"))
|
|
168
|
+
# Save session for conversation continuation
|
|
169
|
+
if "session" in data:
|
|
170
|
+
self._session = data["session"]
|
|
171
|
+
return data.get("result", "")
|
|
172
|
+
|
|
173
|
+
async def _send_relay(self, prompt: str, timeout: float) -> str:
|
|
174
|
+
"""Send request via WebSocket relay."""
|
|
77
175
|
import websockets
|
|
78
176
|
|
|
79
177
|
input_id = str(uuid.uuid4())
|
|
80
|
-
|
|
81
|
-
# Connect to relay input endpoint
|
|
82
178
|
relay_input_url = self._relay_url.replace("/ws/announce", "/ws/input")
|
|
83
179
|
|
|
84
180
|
async with websockets.connect(relay_input_url) as ws:
|
|
85
|
-
|
|
181
|
+
payload = {"prompt": prompt, "to": self.address, "timestamp": int(time.time())}
|
|
182
|
+
signed = self._sign_payload(payload)
|
|
183
|
+
|
|
86
184
|
input_message = {
|
|
87
185
|
"type": "INPUT",
|
|
88
186
|
"input_id": input_id,
|
|
89
187
|
"to": self.address,
|
|
90
|
-
|
|
188
|
+
**signed
|
|
91
189
|
}
|
|
92
190
|
|
|
93
191
|
await ws.send(json.dumps(input_message))
|
|
94
192
|
|
|
95
|
-
# Wait for OUTPUT
|
|
96
193
|
response_data = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
|
97
194
|
response = json.loads(response_data)
|
|
98
195
|
|
|
99
|
-
# Return result
|
|
100
196
|
if response.get("type") == "OUTPUT" and response.get("input_id") == input_id:
|
|
101
197
|
return response.get("result", "")
|
|
102
198
|
elif response.get("type") == "ERROR":
|
|
@@ -104,25 +200,73 @@ class RemoteAgent:
|
|
|
104
200
|
else:
|
|
105
201
|
raise ConnectionError(f"Unexpected response: {response}")
|
|
106
202
|
|
|
203
|
+
async def _send_task(self, prompt: str, timeout: float) -> str:
|
|
204
|
+
"""
|
|
205
|
+
Send task using best available connection method.
|
|
206
|
+
|
|
207
|
+
Priority:
|
|
208
|
+
1. Cached endpoint (if previously successful)
|
|
209
|
+
2. Discovered HTTPS endpoints
|
|
210
|
+
3. Discovered HTTP endpoints
|
|
211
|
+
4. Relay fallback
|
|
212
|
+
"""
|
|
213
|
+
# Try cached endpoint first
|
|
214
|
+
if self._cached_endpoint:
|
|
215
|
+
try:
|
|
216
|
+
return await self._send_http(self._cached_endpoint, prompt, timeout)
|
|
217
|
+
except Exception:
|
|
218
|
+
self._cached_endpoint = None # Clear failed cache
|
|
219
|
+
|
|
220
|
+
# Discover endpoints
|
|
221
|
+
endpoints = await self._discover_endpoints()
|
|
222
|
+
|
|
223
|
+
# Sort: HTTPS first, then HTTP
|
|
224
|
+
endpoints.sort(key=lambda e: (0 if e.startswith("https://") else 1))
|
|
225
|
+
|
|
226
|
+
# Try each endpoint
|
|
227
|
+
for endpoint in endpoints:
|
|
228
|
+
try:
|
|
229
|
+
result = await self._send_http(endpoint, prompt, timeout)
|
|
230
|
+
self._cached_endpoint = endpoint # Cache successful endpoint
|
|
231
|
+
return result
|
|
232
|
+
except Exception:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Fallback to relay
|
|
236
|
+
return await self._send_relay(prompt, timeout)
|
|
237
|
+
|
|
107
238
|
def __repr__(self):
|
|
108
239
|
short = self.address[:12] + "..." if len(self.address) > 12 else self.address
|
|
109
240
|
return f"RemoteAgent({short})"
|
|
110
241
|
|
|
111
242
|
|
|
112
|
-
def connect(
|
|
243
|
+
def connect(
|
|
244
|
+
address: str,
|
|
245
|
+
*,
|
|
246
|
+
keys: Optional[Dict[str, Any]] = None,
|
|
247
|
+
relay_url: str = "wss://oo.openonion.ai/ws/announce"
|
|
248
|
+
) -> RemoteAgent:
|
|
113
249
|
"""
|
|
114
250
|
Connect to a remote agent.
|
|
115
251
|
|
|
116
252
|
Args:
|
|
117
253
|
address: Agent's public key address (0x...)
|
|
254
|
+
keys: Signing keys from address.load() - required for strict trust agents
|
|
118
255
|
relay_url: Relay server URL (default: production)
|
|
119
256
|
|
|
120
257
|
Returns:
|
|
121
258
|
RemoteAgent interface
|
|
122
259
|
|
|
123
260
|
Example:
|
|
124
|
-
>>> from connectonion import connect
|
|
125
|
-
>>>
|
|
126
|
-
>>>
|
|
261
|
+
>>> from connectonion import connect, address
|
|
262
|
+
>>>
|
|
263
|
+
>>> # Simple (unsigned)
|
|
264
|
+
>>> agent = connect("0x3d4017c3...")
|
|
265
|
+
>>> result = agent.input("Hello")
|
|
266
|
+
>>>
|
|
267
|
+
>>> # With signing (for strict trust agents)
|
|
268
|
+
>>> keys = address.load(Path(".co"))
|
|
269
|
+
>>> agent = connect("0x3d4017c3...", keys=keys)
|
|
270
|
+
>>> result = agent.input("Hello")
|
|
127
271
|
"""
|
|
128
|
-
return RemoteAgent(address, relay_url)
|
|
272
|
+
return RemoteAgent(address, keys=keys, relay_url=relay_url)
|
connectonion/host.py
CHANGED
|
@@ -9,7 +9,13 @@ Trust parameter accepts three forms:
|
|
|
9
9
|
3. Agent: Custom Agent instance for verification
|
|
10
10
|
|
|
11
11
|
All forms create a trust agent behind the scenes.
|
|
12
|
+
|
|
13
|
+
Worker Isolation:
|
|
14
|
+
Each request gets a fresh deep copy of the agent template.
|
|
15
|
+
This ensures complete isolation - tools with state (like BrowserTool)
|
|
16
|
+
don't interfere between concurrent requests.
|
|
12
17
|
"""
|
|
18
|
+
import copy
|
|
13
19
|
import hashlib
|
|
14
20
|
import json
|
|
15
21
|
import logging
|
|
@@ -102,17 +108,18 @@ class SessionStorage:
|
|
|
102
108
|
|
|
103
109
|
# === Handlers (pure functions) ===
|
|
104
110
|
|
|
105
|
-
def input_handler(
|
|
111
|
+
def input_handler(agent_template, storage: SessionStorage, prompt: str, result_ttl: int,
|
|
106
112
|
session: dict | None = None) -> dict:
|
|
107
113
|
"""POST /input
|
|
108
114
|
|
|
109
115
|
Args:
|
|
110
|
-
|
|
116
|
+
agent_template: The agent template (deep copied per request for isolation)
|
|
111
117
|
storage: SessionStorage for persisting results
|
|
112
118
|
prompt: The user's prompt
|
|
113
119
|
result_ttl: How long to keep the result on server
|
|
114
120
|
session: Optional conversation session for continuation
|
|
115
121
|
"""
|
|
122
|
+
agent = copy.deepcopy(agent_template)
|
|
116
123
|
now = time.time()
|
|
117
124
|
|
|
118
125
|
# Get or generate session_id
|
|
@@ -368,7 +375,12 @@ def admin_logs_handler(agent_name: str) -> dict:
|
|
|
368
375
|
|
|
369
376
|
|
|
370
377
|
def admin_sessions_handler() -> dict:
|
|
371
|
-
"""GET /admin/sessions - return
|
|
378
|
+
"""GET /admin/sessions - return raw session YAML files as JSON.
|
|
379
|
+
|
|
380
|
+
Returns session files as-is (converted from YAML to JSON). Each session
|
|
381
|
+
contains: name, created, updated, total_cost, total_tokens, turns array.
|
|
382
|
+
Frontend handles the display logic.
|
|
383
|
+
"""
|
|
372
384
|
import yaml
|
|
373
385
|
sessions_dir = Path(".co/sessions")
|
|
374
386
|
if not sessions_dir.exists():
|
|
@@ -381,30 +393,34 @@ def admin_sessions_handler() -> dict:
|
|
|
381
393
|
if session_data:
|
|
382
394
|
sessions.append(session_data)
|
|
383
395
|
|
|
384
|
-
# Sort by
|
|
385
|
-
sessions.sort(key=lambda s: s.get("created", ""), reverse=True)
|
|
396
|
+
# Sort by updated date descending (newest first)
|
|
397
|
+
sessions.sort(key=lambda s: s.get("updated", s.get("created", "")), reverse=True)
|
|
386
398
|
return {"sessions": sessions}
|
|
387
399
|
|
|
388
400
|
|
|
389
401
|
# === Entry Point ===
|
|
390
402
|
|
|
391
|
-
def _create_handlers(
|
|
403
|
+
def _create_handlers(agent_template, result_ttl: int):
|
|
392
404
|
"""Create handler dict for ASGI app."""
|
|
405
|
+
def ws_input(prompt: str) -> str:
|
|
406
|
+
agent = copy.deepcopy(agent_template)
|
|
407
|
+
return agent.input(prompt)
|
|
408
|
+
|
|
393
409
|
return {
|
|
394
|
-
"input": lambda storage, prompt, ttl, session=None: input_handler(
|
|
410
|
+
"input": lambda storage, prompt, ttl, session=None: input_handler(agent_template, storage, prompt, ttl, session),
|
|
395
411
|
"session": session_handler,
|
|
396
412
|
"sessions": sessions_handler,
|
|
397
|
-
"health": lambda start_time: health_handler(
|
|
398
|
-
"info": lambda trust: info_handler(
|
|
413
|
+
"health": lambda start_time: health_handler(agent_template, start_time),
|
|
414
|
+
"info": lambda trust: info_handler(agent_template, trust),
|
|
399
415
|
"auth": extract_and_authenticate,
|
|
400
|
-
"ws_input":
|
|
416
|
+
"ws_input": ws_input,
|
|
401
417
|
# Admin endpoints (auth required via OPENONION_API_KEY)
|
|
402
|
-
"admin_logs": lambda: admin_logs_handler(
|
|
418
|
+
"admin_logs": lambda: admin_logs_handler(agent_template.name),
|
|
403
419
|
"admin_sessions": admin_sessions_handler,
|
|
404
420
|
}
|
|
405
421
|
|
|
406
422
|
|
|
407
|
-
def _start_relay_background(
|
|
423
|
+
def _start_relay_background(agent_template, relay_url: str, addr_data: dict):
|
|
408
424
|
"""Start relay connection in background thread.
|
|
409
425
|
|
|
410
426
|
The relay connection runs alongside the HTTP server, allowing the agent
|
|
@@ -415,11 +431,12 @@ def _start_relay_background(agent, relay_url: str, addr_data: dict):
|
|
|
415
431
|
from . import announce, relay
|
|
416
432
|
|
|
417
433
|
# Create ANNOUNCE message
|
|
418
|
-
summary =
|
|
434
|
+
summary = agent_template.system_prompt[:1000] if agent_template.system_prompt else f"{agent_template.name} agent"
|
|
419
435
|
announce_msg = announce.create_announce_message(addr_data, summary, endpoints=[])
|
|
420
436
|
|
|
421
|
-
# Task handler
|
|
437
|
+
# Task handler - deep copy for each request
|
|
422
438
|
async def task_handler(prompt: str) -> str:
|
|
439
|
+
agent = copy.deepcopy(agent_template)
|
|
423
440
|
return agent.input(prompt)
|
|
424
441
|
|
|
425
442
|
async def relay_loop():
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Audio transcription utility using Gemini API
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [os, base64, pathlib, mimetypes, google.generativeai] | imported by [user code] | tested by [tests/test_transcribe.py]
|
|
5
|
+
Data flow: transcribe(audio, prompt, model) → load audio file → encode base64 → call Gemini API → return text
|
|
6
|
+
State/Effects: reads audio files from disk | makes Gemini API request | no caching
|
|
7
|
+
Integration: exposes transcribe(audio, prompt, model, timestamps) | similar pattern to llm_do()
|
|
8
|
+
Performance: one API call per transcription | files < 20MB use inline, larger use File API
|
|
9
|
+
Errors: raises ValueError if audio file not found | FileNotFoundError for missing files
|
|
10
|
+
|
|
11
|
+
Audio transcription utility for ConnectOnion framework.
|
|
12
|
+
|
|
13
|
+
This module provides the `transcribe()` function - a simple interface for
|
|
14
|
+
converting audio files to text using Gemini's multimodal capabilities.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
>>> from connectonion import transcribe
|
|
18
|
+
|
|
19
|
+
# Simple transcription
|
|
20
|
+
>>> text = transcribe("meeting.mp3")
|
|
21
|
+
|
|
22
|
+
# With context hints (improve accuracy for domain-specific terms)
|
|
23
|
+
>>> text = transcribe("meeting.mp3", prompt="Technical AI discussion, speakers: Aaron, Lisa")
|
|
24
|
+
|
|
25
|
+
# Different model
|
|
26
|
+
>>> text = transcribe("meeting.mp3", model="co/gemini-2.5-flash")
|
|
27
|
+
|
|
28
|
+
Supported formats: WAV, MP3, AIFF, AAC, OGG, FLAC
|
|
29
|
+
Token cost: 32 tokens per second of audio (1 minute = 1,920 tokens)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import base64
|
|
34
|
+
import mimetypes
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Optional
|
|
37
|
+
import httpx
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# MIME type mapping for audio formats
|
|
41
|
+
AUDIO_MIME_TYPES = {
|
|
42
|
+
".wav": "audio/wav",
|
|
43
|
+
".mp3": "audio/mp3",
|
|
44
|
+
".aiff": "audio/aiff",
|
|
45
|
+
".aac": "audio/aac",
|
|
46
|
+
".ogg": "audio/ogg",
|
|
47
|
+
".flac": "audio/flac",
|
|
48
|
+
".m4a": "audio/mp4",
|
|
49
|
+
".webm": "audio/webm",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Maximum file size for inline audio (20MB)
|
|
53
|
+
MAX_INLINE_SIZE = 20 * 1024 * 1024
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_mime_type(file_path: Path) -> str:
|
|
57
|
+
"""Get MIME type for audio file."""
|
|
58
|
+
suffix = file_path.suffix.lower()
|
|
59
|
+
if suffix in AUDIO_MIME_TYPES:
|
|
60
|
+
return AUDIO_MIME_TYPES[suffix]
|
|
61
|
+
# Fallback to mimetypes library
|
|
62
|
+
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
63
|
+
return mime_type or "audio/mpeg"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_api_key(model: str) -> str:
|
|
67
|
+
"""Get API key based on model."""
|
|
68
|
+
if model.startswith("co/"):
|
|
69
|
+
# Use OpenOnion managed keys
|
|
70
|
+
api_key = os.getenv("OPENONION_API_KEY")
|
|
71
|
+
if not api_key:
|
|
72
|
+
# Try loading from config file
|
|
73
|
+
config_path = Path.home() / ".connectonion" / ".co" / "config.toml"
|
|
74
|
+
if config_path.exists():
|
|
75
|
+
import toml
|
|
76
|
+
config = toml.load(config_path)
|
|
77
|
+
api_key = config.get("auth", {}).get("jwt_token")
|
|
78
|
+
if not api_key:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"OpenOnion API key required for co/ models. "
|
|
81
|
+
"Run `co auth` to authenticate or set OPENONION_API_KEY."
|
|
82
|
+
)
|
|
83
|
+
return api_key
|
|
84
|
+
else:
|
|
85
|
+
# Use Gemini API key directly
|
|
86
|
+
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
|
|
87
|
+
if not api_key:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"Gemini API key required. Set GEMINI_API_KEY environment variable."
|
|
90
|
+
)
|
|
91
|
+
return api_key
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def transcribe(
|
|
95
|
+
audio: str,
|
|
96
|
+
prompt: Optional[str] = None,
|
|
97
|
+
model: str = "co/gemini-3-flash-preview",
|
|
98
|
+
timestamps: bool = False,
|
|
99
|
+
) -> str:
|
|
100
|
+
"""
|
|
101
|
+
Transcribe audio file to text using Gemini.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
audio: Path to audio file (WAV, MP3, AIFF, AAC, OGG, FLAC)
|
|
105
|
+
prompt: Optional context hints for better accuracy
|
|
106
|
+
(e.g., "Technical AI discussion, speakers: Aaron, Lisa")
|
|
107
|
+
model: Model to use (default: co/gemini-3-flash-preview)
|
|
108
|
+
timestamps: If True, include timestamps in output
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Transcribed text
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
>>> # Simple transcription
|
|
115
|
+
>>> text = transcribe("meeting.mp3")
|
|
116
|
+
|
|
117
|
+
>>> # With context hints
|
|
118
|
+
>>> text = transcribe("meeting.mp3", prompt="Fix: ConnectOnion, OpenOnion")
|
|
119
|
+
|
|
120
|
+
>>> # With timestamps
|
|
121
|
+
>>> text = transcribe("podcast.mp3", timestamps=True)
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
FileNotFoundError: If audio file doesn't exist
|
|
125
|
+
ValueError: If API key is missing or invalid audio format
|
|
126
|
+
"""
|
|
127
|
+
# Validate file exists
|
|
128
|
+
file_path = Path(audio)
|
|
129
|
+
if not file_path.exists():
|
|
130
|
+
raise FileNotFoundError(f"Audio file not found: {audio}")
|
|
131
|
+
|
|
132
|
+
# Get file info
|
|
133
|
+
file_size = file_path.stat().st_size
|
|
134
|
+
mime_type = _get_mime_type(file_path)
|
|
135
|
+
|
|
136
|
+
# Read and encode audio
|
|
137
|
+
with open(file_path, "rb") as f:
|
|
138
|
+
audio_bytes = f.read()
|
|
139
|
+
audio_base64 = base64.standard_b64encode(audio_bytes).decode("utf-8")
|
|
140
|
+
|
|
141
|
+
# Build prompt
|
|
142
|
+
if timestamps:
|
|
143
|
+
system_prompt = "Transcribe this audio with timestamps in [MM:SS] format."
|
|
144
|
+
else:
|
|
145
|
+
system_prompt = "Transcribe this audio accurately."
|
|
146
|
+
|
|
147
|
+
if prompt:
|
|
148
|
+
system_prompt += f" Context: {prompt}"
|
|
149
|
+
|
|
150
|
+
# Get API key and model name
|
|
151
|
+
api_key = _get_api_key(model)
|
|
152
|
+
actual_model = model[3:] if model.startswith("co/") else model
|
|
153
|
+
|
|
154
|
+
# Use OpenOnion proxy for co/ models, direct Gemini API otherwise
|
|
155
|
+
if model.startswith("co/"):
|
|
156
|
+
return _transcribe_via_openonion(
|
|
157
|
+
audio_base64, mime_type, system_prompt, api_key, actual_model
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
return _transcribe_via_gemini(
|
|
161
|
+
audio_base64, mime_type, system_prompt, api_key, actual_model
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _transcribe_via_gemini(
|
|
166
|
+
audio_base64: str,
|
|
167
|
+
mime_type: str,
|
|
168
|
+
prompt: str,
|
|
169
|
+
api_key: str,
|
|
170
|
+
model: str,
|
|
171
|
+
) -> str:
|
|
172
|
+
"""Transcribe using Gemini's OpenAI-compatible endpoint."""
|
|
173
|
+
import openai
|
|
174
|
+
|
|
175
|
+
client = openai.OpenAI(
|
|
176
|
+
api_key=api_key,
|
|
177
|
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
response = client.chat.completions.create(
|
|
181
|
+
model=model,
|
|
182
|
+
messages=[
|
|
183
|
+
{
|
|
184
|
+
"role": "user",
|
|
185
|
+
"content": [
|
|
186
|
+
{"type": "text", "text": prompt},
|
|
187
|
+
{
|
|
188
|
+
"type": "input_audio",
|
|
189
|
+
"input_audio": {
|
|
190
|
+
"data": audio_base64,
|
|
191
|
+
"format": mime_type.split("/")[-1], # e.g., "mp3"
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return response.choices[0].message.content
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _transcribe_via_openonion(
|
|
203
|
+
audio_base64: str,
|
|
204
|
+
mime_type: str,
|
|
205
|
+
prompt: str,
|
|
206
|
+
api_key: str,
|
|
207
|
+
model: str,
|
|
208
|
+
) -> str:
|
|
209
|
+
"""Transcribe using OpenOnion proxy (for co/ models)."""
|
|
210
|
+
# Determine API URL
|
|
211
|
+
is_dev = os.getenv("OPENONION_DEV") or os.getenv("ENVIRONMENT") == "development"
|
|
212
|
+
base_url = "http://localhost:8000" if is_dev else "https://oo.openonion.ai"
|
|
213
|
+
|
|
214
|
+
# Build request
|
|
215
|
+
request_body = {
|
|
216
|
+
"model": model,
|
|
217
|
+
"messages": [
|
|
218
|
+
{
|
|
219
|
+
"role": "user",
|
|
220
|
+
"content": [
|
|
221
|
+
{"type": "text", "text": prompt},
|
|
222
|
+
{
|
|
223
|
+
"type": "input_audio",
|
|
224
|
+
"input_audio": {
|
|
225
|
+
"data": audio_base64,
|
|
226
|
+
"format": mime_type.split("/")[-1],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
}
|
|
231
|
+
],
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
response = httpx.post(
|
|
235
|
+
f"{base_url}/v1/chat/completions",
|
|
236
|
+
json=request_body,
|
|
237
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
238
|
+
timeout=120.0,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if response.status_code != 200:
|
|
242
|
+
raise ValueError(f"Transcription failed: {response.status_code} - {response.text}")
|
|
243
|
+
|
|
244
|
+
data = response.json()
|
|
245
|
+
return data["choices"][0]["message"]["content"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: connectonion
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.10
|
|
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,15 +1,15 @@
|
|
|
1
|
-
connectonion/__init__.py,sha256=
|
|
1
|
+
connectonion/__init__.py,sha256=VHZugKSYm3j_qeLiLS93Ee7mqJJ0uPvl9BtLRkKJkBE,1941
|
|
2
2
|
connectonion/address.py,sha256=YOzpMOej-HqJUE6o0i0fG8rB7HM-Iods36s9OD--5ig,10852
|
|
3
3
|
connectonion/agent.py,sha256=BHnP4N0odXCSn9xT0QnJpXj3VV-E_vUtNXJ0M6k3RNs,18889
|
|
4
4
|
connectonion/announce.py,sha256=47Lxe8S4yyTbpsmYUmakU_DehrGvljyldmPfKnAOrFQ,3365
|
|
5
5
|
connectonion/asgi.py,sha256=VTMwwEWLq5RYvIufRImMcv-AB5D5pZmM8INaXqwUt4Q,9621
|
|
6
6
|
connectonion/auto_debug_exception.py,sha256=iA-b5GC40AI4YVXen2UCkDkfHLVZehFPgZrinIxykWI,8147
|
|
7
|
-
connectonion/connect.py,sha256=
|
|
7
|
+
connectonion/connect.py,sha256=dYuBKxUvcNeyR0IbcKrnspwylp7177JmBYz5DsRhiXQ,9794
|
|
8
8
|
connectonion/console.py,sha256=6_J1ItkLJCHDpdJY-tCuw4jMcS09S-a5juZSDSIr1Nc,21254
|
|
9
9
|
connectonion/debugger_ui.py,sha256=QMyoZkhGbt-pStHHjpnCBXtfzvfqPW94tXiL07KZiAw,41741
|
|
10
10
|
connectonion/decorators.py,sha256=YFmZMptcchIgNriKFf_vOyacor5i_j6Cy_igTJhdKm4,7141
|
|
11
11
|
connectonion/events.py,sha256=jJOMt1PhRl-ef4R8-WpAq8pUbZ8GKIl0wOB2kJUVyWg,9151
|
|
12
|
-
connectonion/host.py,sha256=
|
|
12
|
+
connectonion/host.py,sha256=eVB907_oFcn2MH-8fRnhR-_-o17S7JjchkggPS5kLVA,19437
|
|
13
13
|
connectonion/interactive_debugger.py,sha256=XHSCGJp9YV6VAZM1gk_AuxKAdBODJQUcLVWaLuTMqv0,16277
|
|
14
14
|
connectonion/llm.py,sha256=IzjlOT6Dj5xIplIymjcaHZ_abwE5wDHEE4pa8zjlEGY,32952
|
|
15
15
|
connectonion/llm_do.py,sha256=YI7kGFKpB6KUEG_CKtP3Bl4IWmRac7TCa9GK5FSV624,12000
|
|
@@ -19,6 +19,7 @@ connectonion/relay.py,sha256=a8wj4UZDZpGEhvpyDuRjZJg1S1VzxqiPioQRLq1EAao,7160
|
|
|
19
19
|
connectonion/tool_executor.py,sha256=IPCVRS20XOd5JsZrdXRwK-gI6Xb0BtDQ3fCD0qt2dcE,10617
|
|
20
20
|
connectonion/tool_factory.py,sha256=vo4_pyhqKG_OtfpjV15oPG4zlkxn40768PW5pQVtx-g,6704
|
|
21
21
|
connectonion/tool_registry.py,sha256=rTe8RJnWXpmHXWznP468fhWvmRknk-TlF9Iz8G9_qus,4367
|
|
22
|
+
connectonion/transcribe.py,sha256=m2qd7A2qMKFAbmbLLgvcd-yUFz8FHgjUKtvtFffnK00,7730
|
|
22
23
|
connectonion/trust.py,sha256=It4ueuMvCmHOD7FwaV-jSwlonWFsFgNH1gz9fJtmfW4,6692
|
|
23
24
|
connectonion/trust_agents.py,sha256=XDedEhxGRfu9KeYhX-Z1a5tRA-2Zbwz4ztnlA2Lnaf0,2968
|
|
24
25
|
connectonion/trust_functions.py,sha256=jRgfPm5z8oy9x8IYJd20UUMCz7cc1Pd7zyqQK5SDdLc,3564
|
|
@@ -26,13 +27,14 @@ connectonion/usage.py,sha256=mS_J5it9NMDI1CjycNUJEnXGNXAo1lQ1ICEZvqftTpU,6205
|
|
|
26
27
|
connectonion/xray.py,sha256=TA_VychqQRtfSzO3RmTqnDZZNx3LjycbgUWVyMswAIw,18542
|
|
27
28
|
connectonion/cli/__init__.py,sha256=Pd1iKp0rUghs2kmdAhyp9hK8j0uWkNxOagBkdCGrG4I,55
|
|
28
29
|
connectonion/cli/docs.md,sha256=Fk_JT8jFXXDpXTvU0ZUigMW1UR6ERmX-HDheYPPRNY8,3231
|
|
29
|
-
connectonion/cli/main.py,sha256=
|
|
30
|
+
connectonion/cli/main.py,sha256=2FWi-yhnyLQzZl_esOXYY3-u0Q1wbpdE4XG3QYoGm_Q,6893
|
|
30
31
|
connectonion/cli/browser_agent/__init__.py,sha256=xZCoxS3dGVBBMnaasHOE1vxMDdIwUloRP67rGR-4obo,173
|
|
31
32
|
connectonion/cli/browser_agent/browser.py,sha256=EdBAvpcfCHh8q6_-3-LVBw6OHyMYnz1_8m6gryWQdzM,8830
|
|
32
33
|
connectonion/cli/browser_agent/prompt.md,sha256=tF0PhGcl3gkmVK1F5SG9GZwnPPXTZYWEaatAZtOYZZg,4479
|
|
33
34
|
connectonion/cli/commands/__init__.py,sha256=IPZy9NwrE0hs4f7hIqyFM9GjFZpeCS8mC0Jf-5Ooy4c,43
|
|
34
35
|
connectonion/cli/commands/auth_commands.py,sha256=D76_0yd77d23bXRvsTAY6HOcGJswo9-6z2dRi9CR9sE,21635
|
|
35
36
|
connectonion/cli/commands/browser_commands.py,sha256=lB4N6XwP43qdcthwQWlbFU2S3INfxhRDXznAw1PSTsQ,1803
|
|
37
|
+
connectonion/cli/commands/copy_commands.py,sha256=9cRpTFlBKrFXgJw582YJqKLq6o1dmwPB_c9RHEJc76c,3852
|
|
36
38
|
connectonion/cli/commands/create.py,sha256=0TrsPulS6dqwr_gtIWWBQ03Q_BnNro-CV4qOV5VkHAg,22011
|
|
37
39
|
connectonion/cli/commands/deploy_commands.py,sha256=s0bjApUQbNlMGStUSrAB8RqSSewBlHrBL60NWgEIHhc,8248
|
|
38
40
|
connectonion/cli/commands/doctor_commands.py,sha256=EOk8CMclvVqLq4-Dg2JghWehH9VQnthBejrhIBX66_4,7244
|
|
@@ -107,7 +109,7 @@ connectonion/useful_tools/slash_command.py,sha256=VKl7SKLyaxHcHwfE8rgzBCQ2-KQtL0
|
|
|
107
109
|
connectonion/useful_tools/terminal.py,sha256=PtzGHN6vnWROmssi37YOZ5U-d0Z7Fe79bocoKAngoxg,9330
|
|
108
110
|
connectonion/useful_tools/todo_list.py,sha256=wVEt4kt-MczIcP3xvKelfshGHZ6EATpDFzQx0zov8cw,7483
|
|
109
111
|
connectonion/useful_tools/web_fetch.py,sha256=CysJLOdDIkbMVJ12Dj3WgsytLu1-o2wrwNopqA_S1jk,7253
|
|
110
|
-
connectonion-0.5.
|
|
111
|
-
connectonion-0.5.
|
|
112
|
-
connectonion-0.5.
|
|
113
|
-
connectonion-0.5.
|
|
112
|
+
connectonion-0.5.10.dist-info/METADATA,sha256=1xQJXemh__kTUwluT5sYBIG0FasMf2mfdmLd1QszJxg,21903
|
|
113
|
+
connectonion-0.5.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
114
|
+
connectonion-0.5.10.dist-info/entry_points.txt,sha256=XDB-kVN7Qgy4DmYTkjQB_O6hZeUND-SqmZbdoQPn6WA,90
|
|
115
|
+
connectonion-0.5.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|