connectonion 0.5.7__tar.gz → 0.5.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. {connectonion-0.5.7 → connectonion-0.5.9}/PKG-INFO +1 -1
  2. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/__init__.py +3 -1
  3. connectonion-0.5.9/connectonion/cli/commands/copy_commands.py +116 -0
  4. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/main.py +14 -1
  5. connectonion-0.5.9/connectonion/connect.py +272 -0
  6. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/host.py +8 -3
  7. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/static/docs.html +97 -0
  8. connectonion-0.5.9/connectonion/transcribe.py +245 -0
  9. {connectonion-0.5.7 → connectonion-0.5.9}/docs/cli/README.md +47 -0
  10. {connectonion-0.5.7 → connectonion-0.5.9}/docs/useful_plugins/README.md +24 -0
  11. {connectonion-0.5.7 → connectonion-0.5.9}/docs/useful_tools/README.md +24 -0
  12. {connectonion-0.5.7 → connectonion-0.5.9}/pyproject.toml +1 -1
  13. connectonion-0.5.7/connectonion/connect.py +0 -128
  14. {connectonion-0.5.7 → connectonion-0.5.9}/.gitignore +0 -0
  15. {connectonion-0.5.7 → connectonion-0.5.9}/README.md +0 -0
  16. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/address.py +0 -0
  17. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/agent.py +0 -0
  18. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/announce.py +0 -0
  19. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/asgi.py +0 -0
  20. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/auto_debug_exception.py +0 -0
  21. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/__init__.py +0 -0
  22. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/browser_agent/__init__.py +0 -0
  23. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/browser_agent/browser.py +0 -0
  24. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/browser_agent/prompt.md +0 -0
  25. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/commands/__init__.py +0 -0
  26. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/commands/auth_commands.py +0 -0
  27. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/commands/browser_commands.py +0 -0
  28. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/commands/create.py +0 -0
  29. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/commands/deploy_commands.py +0 -0
  30. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/commands/doctor_commands.py +0 -0
  31. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/commands/init.py +0 -0
  32. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/commands/project_cmd_lib.py +0 -0
  33. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/commands/reset_commands.py +0 -0
  34. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/commands/status_commands.py +0 -0
  35. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +0 -0
  36. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/docs/connectonion.md +0 -0
  37. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/docs.md +0 -0
  38. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/meta-agent/README.md +0 -0
  39. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/meta-agent/agent.py +0 -0
  40. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +0 -0
  41. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +0 -0
  42. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/meta-agent/prompts/metagent.md +0 -0
  43. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/meta-agent/prompts/think_prompt.md +0 -0
  44. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/minimal/README.md +0 -0
  45. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/minimal/agent.py +0 -0
  46. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/playwright/README.md +0 -0
  47. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/playwright/agent.py +0 -0
  48. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/playwright/prompt.md +0 -0
  49. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/playwright/requirements.txt +0 -0
  50. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/cli/templates/web-research/agent.py +0 -0
  51. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/console.py +0 -0
  52. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/debug_agent/__init__.py +0 -0
  53. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/debug_agent/agent.py +0 -0
  54. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/debug_agent/prompts/debug_assistant.md +0 -0
  55. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/debug_agent/runtime_inspector.py +0 -0
  56. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/debug_explainer/__init__.py +0 -0
  57. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/debug_explainer/explain_agent.py +0 -0
  58. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/debug_explainer/explain_context.py +0 -0
  59. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/debug_explainer/explainer_prompt.md +0 -0
  60. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/debug_explainer/root_cause_analysis_prompt.md +0 -0
  61. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/debugger_ui.py +0 -0
  62. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/decorators.py +0 -0
  63. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/events.py +0 -0
  64. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/execution_analyzer/__init__.py +0 -0
  65. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/execution_analyzer/execution_analysis.py +0 -0
  66. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/execution_analyzer/execution_analysis_prompt.md +0 -0
  67. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/interactive_debugger.py +0 -0
  68. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/llm.py +0 -0
  69. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/llm_do.py +0 -0
  70. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/logger.py +0 -0
  71. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/prompt_files/__init__.py +0 -0
  72. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/prompt_files/analyze_contact.md +0 -0
  73. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/prompt_files/eval_expected.md +0 -0
  74. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/prompt_files/react_evaluate.md +0 -0
  75. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/prompt_files/react_plan.md +0 -0
  76. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/prompt_files/reflect.md +0 -0
  77. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/prompts.py +0 -0
  78. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/relay.py +0 -0
  79. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tool_executor.py +0 -0
  80. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tool_factory.py +0 -0
  81. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tool_registry.py +0 -0
  82. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/trust.py +0 -0
  83. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/trust_agents.py +0 -0
  84. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/trust_functions.py +0 -0
  85. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tui/__init__.py +0 -0
  86. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tui/divider.py +0 -0
  87. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tui/dropdown.py +0 -0
  88. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tui/footer.py +0 -0
  89. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tui/fuzzy.py +0 -0
  90. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tui/input.py +0 -0
  91. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tui/keys.py +0 -0
  92. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tui/pick.py +0 -0
  93. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tui/providers.py +0 -0
  94. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/tui/status_bar.py +0 -0
  95. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/usage.py +0 -0
  96. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_events_handlers/__init__.py +0 -0
  97. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_events_handlers/reflect.py +0 -0
  98. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_plugins/__init__.py +0 -0
  99. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_plugins/calendar_plugin.py +0 -0
  100. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_plugins/eval.py +0 -0
  101. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_plugins/gmail_plugin.py +0 -0
  102. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_plugins/image_result_formatter.py +0 -0
  103. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_plugins/re_act.py +0 -0
  104. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_plugins/shell_approval.py +0 -0
  105. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/__init__.py +0 -0
  106. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/diff_writer.py +0 -0
  107. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/get_emails.py +0 -0
  108. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/gmail.py +0 -0
  109. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/google_calendar.py +0 -0
  110. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/memory.py +0 -0
  111. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/microsoft_calendar.py +0 -0
  112. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/outlook.py +0 -0
  113. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/send_email.py +0 -0
  114. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/shell.py +0 -0
  115. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/slash_command.py +0 -0
  116. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/terminal.py +0 -0
  117. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/todo_list.py +0 -0
  118. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/useful_tools/web_fetch.py +0 -0
  119. {connectonion-0.5.7 → connectonion-0.5.9}/connectonion/xray.py +0 -0
  120. {connectonion-0.5.7 → connectonion-0.5.9}/docs/README.md +0 -0
  121. {connectonion-0.5.7 → connectonion-0.5.9}/docs/debug/README.md +0 -0
  122. {connectonion-0.5.7 → connectonion-0.5.9}/docs/integrations/README.md +0 -0
  123. {connectonion-0.5.7 → connectonion-0.5.9}/docs/network/README.md +0 -0
  124. {connectonion-0.5.7 → connectonion-0.5.9}/docs/templates/README.md +0 -0
  125. {connectonion-0.5.7 → connectonion-0.5.9}/docs/tui/README.md +0 -0
  126. {connectonion-0.5.7 → connectonion-0.5.9}/examples/README.md +0 -0
  127. {connectonion-0.5.7 → connectonion-0.5.9}/examples/browser-agent/README.md +0 -0
  128. {connectonion-0.5.7 → connectonion-0.5.9}/examples/email-agent/README.md +0 -0
  129. {connectonion-0.5.7 → connectonion-0.5.9}/examples/simple-agent/README.md +0 -0
  130. {connectonion-0.5.7 → connectonion-0.5.9}/prompts/README.md +0 -0
  131. {connectonion-0.5.7 → connectonion-0.5.9}/prompts/formats/README.md +0 -0
  132. {connectonion-0.5.7 → connectonion-0.5.9}/tests/README.md +0 -0
  133. {connectonion-0.5.7 → connectonion-0.5.9}/tests/cli/README.md +0 -0
  134. {connectonion-0.5.7 → connectonion-0.5.9}/tests/cli/aws/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: connectonion
3
- Version: 0.5.7
3
+ Version: 0.5.9
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,6 +1,6 @@
1
1
  """ConnectOnion - A simple agent framework with behavior tracking."""
2
2
 
3
- __version__ = "0.5.7"
3
+ __version__ = "0.5.9"
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]")
@@ -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()
@@ -0,0 +1,272 @@
1
+ """
2
+ Purpose: Client interface for connecting to remote agents via HTTP or relay network
3
+ LLM-Note:
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
9
+
10
+ Connect to remote agents on the network.
11
+
12
+ Smart discovery: tries HTTP endpoints first, falls back to relay.
13
+ Always signs requests when keys are provided.
14
+ """
15
+
16
+ import asyncio
17
+ import json
18
+ import time
19
+ import uuid
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ from . import address as addr
23
+
24
+
25
+ class RemoteAgent:
26
+ """
27
+ Interface to a remote agent.
28
+
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")
44
+ """
45
+
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
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
58
+
59
+ def input(self, prompt: str, timeout: float = 30.0) -> str:
60
+ """
61
+ Send task to remote agent and get response (sync version).
62
+
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.
68
+
69
+ Args:
70
+ prompt: Task/prompt to send
71
+ timeout: Seconds to wait for response (default 30)
72
+
73
+ Returns:
74
+ Agent's response string
75
+
76
+ Raises:
77
+ RuntimeError: If called from within a running event loop
78
+
79
+ Example:
80
+ >>> translator = connect("0x3d40...")
81
+ >>> result = translator.input("Translate 'hello' to Spanish")
82
+ >>> # Continue conversation
83
+ >>> result2 = translator.input("Now translate it to French")
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
95
+ return asyncio.run(self._send_task(prompt, timeout))
96
+
97
+ async def input_async(self, prompt: str, timeout: float = 30.0) -> str:
98
+ """
99
+ Send task to remote agent and get response (async version).
100
+
101
+ Automatically maintains conversation context across calls.
102
+
103
+ Args:
104
+ prompt: Task/prompt to send
105
+ timeout: Seconds to wait for response (default 30)
106
+
107
+ Returns:
108
+ Agent's response string
109
+ """
110
+ return await self._send_task(prompt, timeout)
111
+
112
+ def reset_conversation(self):
113
+ """Clear conversation history and start fresh."""
114
+ self._session = None
115
+
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."""
175
+ import websockets
176
+
177
+ input_id = str(uuid.uuid4())
178
+ relay_input_url = self._relay_url.replace("/ws/announce", "/ws/input")
179
+
180
+ async with websockets.connect(relay_input_url) as ws:
181
+ payload = {"prompt": prompt, "to": self.address, "timestamp": int(time.time())}
182
+ signed = self._sign_payload(payload)
183
+
184
+ input_message = {
185
+ "type": "INPUT",
186
+ "input_id": input_id,
187
+ "to": self.address,
188
+ **signed
189
+ }
190
+
191
+ await ws.send(json.dumps(input_message))
192
+
193
+ response_data = await asyncio.wait_for(ws.recv(), timeout=timeout)
194
+ response = json.loads(response_data)
195
+
196
+ if response.get("type") == "OUTPUT" and response.get("input_id") == input_id:
197
+ return response.get("result", "")
198
+ elif response.get("type") == "ERROR":
199
+ raise ConnectionError(f"Agent error: {response.get('error')}")
200
+ else:
201
+ raise ConnectionError(f"Unexpected response: {response}")
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
+
238
+ def __repr__(self):
239
+ short = self.address[:12] + "..." if len(self.address) > 12 else self.address
240
+ return f"RemoteAgent({short})"
241
+
242
+
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:
249
+ """
250
+ Connect to a remote agent.
251
+
252
+ Args:
253
+ address: Agent's public key address (0x...)
254
+ keys: Signing keys from address.load() - required for strict trust agents
255
+ relay_url: Relay server URL (default: production)
256
+
257
+ Returns:
258
+ RemoteAgent interface
259
+
260
+ Example:
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")
271
+ """
272
+ return RemoteAgent(address, keys=keys, relay_url=relay_url)
@@ -368,7 +368,12 @@ def admin_logs_handler(agent_name: str) -> dict:
368
368
 
369
369
 
370
370
  def admin_sessions_handler() -> dict:
371
- """GET /admin/sessions - return all activity sessions as JSON array."""
371
+ """GET /admin/sessions - return raw session YAML files as JSON.
372
+
373
+ Returns session files as-is (converted from YAML to JSON). Each session
374
+ contains: name, created, updated, total_cost, total_tokens, turns array.
375
+ Frontend handles the display logic.
376
+ """
372
377
  import yaml
373
378
  sessions_dir = Path(".co/sessions")
374
379
  if not sessions_dir.exists():
@@ -381,8 +386,8 @@ def admin_sessions_handler() -> dict:
381
386
  if session_data:
382
387
  sessions.append(session_data)
383
388
 
384
- # Sort by created date descending (newest first)
385
- sessions.sort(key=lambda s: s.get("created", ""), reverse=True)
389
+ # Sort by updated date descending (newest first)
390
+ sessions.sort(key=lambda s: s.get("updated", s.get("created", "")), reverse=True)
386
391
  return {"sessions": sessions}
387
392
 
388
393
 
@@ -366,6 +366,70 @@
366
366
  </div>
367
367
  </details>
368
368
 
369
+ <!-- Admin Endpoints Header -->
370
+ <div style="margin:24px 0 12px;padding:12px 16px;background:var(--panel);border:1px solid var(--panel-border);border-radius:4px;">
371
+ <div class="row between">
372
+ <div>
373
+ <h3 style="margin:0;font-size:14px;">Admin Endpoints</h3>
374
+ <p style="margin:4px 0 0;font-size:12px;color:var(--muted);">Requires OPENONION_API_KEY authorization</p>
375
+ </div>
376
+ <div class="row" style="gap:8px;">
377
+ <label for="api-key" style="font-size:12px;margin:0;">API Key:</label>
378
+ <input type="password" id="api-key" placeholder="Enter API key..." style="width:200px;height:36px;font-size:12px;" />
379
+ </div>
380
+ </div>
381
+ </div>
382
+
383
+ <!-- GET /admin/logs -->
384
+ <details class="endpoint">
385
+ <summary class="endpoint-header">
386
+ <span class="method get">GET</span>
387
+ <span class="endpoint-path">/admin/logs</span>
388
+ <span class="endpoint-desc">Get agent activity logs (auth required)</span>
389
+ <span class="endpoint-expand">▾</span>
390
+ </summary>
391
+ <div class="endpoint-body">
392
+ <button class="btn execute" onclick="fetchAdminLogs()">Execute</button>
393
+ <div class="endpoint-section" style="margin-top:12px;">
394
+ <h4>Curl</h4>
395
+ <div class="curl-box">
396
+ <pre id="admin-logs-curl">curl -X GET "http://localhost:8000/admin/logs" \
397
+ -H "Authorization: Bearer YOUR_API_KEY"</pre>
398
+ <button class="copy-btn" onclick="copyCurl('admin-logs-curl')">Copy</button>
399
+ </div>
400
+ </div>
401
+ <div class="endpoint-section">
402
+ <h4>Response</h4>
403
+ <pre id="admin-logs-result" style="max-height:400px;">{ }</pre>
404
+ </div>
405
+ </div>
406
+ </details>
407
+
408
+ <!-- GET /admin/sessions -->
409
+ <details class="endpoint">
410
+ <summary class="endpoint-header">
411
+ <span class="method get">GET</span>
412
+ <span class="endpoint-path">/admin/sessions</span>
413
+ <span class="endpoint-desc">Get all activity sessions (auth required)</span>
414
+ <span class="endpoint-expand">▾</span>
415
+ </summary>
416
+ <div class="endpoint-body">
417
+ <button class="btn execute" onclick="fetchAdminSessions()">Execute</button>
418
+ <div class="endpoint-section" style="margin-top:12px;">
419
+ <h4>Curl</h4>
420
+ <div class="curl-box">
421
+ <pre id="admin-sessions-curl">curl -X GET "http://localhost:8000/admin/sessions" \
422
+ -H "Authorization: Bearer YOUR_API_KEY"</pre>
423
+ <button class="copy-btn" onclick="copyCurl('admin-sessions-curl')">Copy</button>
424
+ </div>
425
+ </div>
426
+ <div class="endpoint-section">
427
+ <h4>Response</h4>
428
+ <pre id="admin-sessions-result" style="max-height:400px;">{ }</pre>
429
+ </div>
430
+ </div>
431
+ </details>
432
+
369
433
  <!-- WebSocket /ws -->
370
434
  <details class="endpoint">
371
435
  <summary class="endpoint-header">
@@ -580,6 +644,39 @@
580
644
  loadInfo(); refreshSessions();
581
645
  }
582
646
 
647
+ async function fetchAdminLogs(){
648
+ const apiKey = $('api-key').value.trim();
649
+ if (!apiKey) {
650
+ $('admin-logs-result').textContent = '{"error": "Please enter API key"}';
651
+ return;
652
+ }
653
+ $('admin-logs-result').textContent = 'Loading...';
654
+ const res = await fetch(buildUrl('/admin/logs'), {
655
+ headers: { 'Authorization': 'Bearer ' + apiKey }
656
+ });
657
+ if (res.ok) {
658
+ const text = await res.text();
659
+ $('admin-logs-result').textContent = text || '(empty)';
660
+ } else {
661
+ const data = await res.json().catch(() => ({ error: res.statusText }));
662
+ $('admin-logs-result').textContent = JSON.stringify(data, null, 2);
663
+ }
664
+ }
665
+
666
+ async function fetchAdminSessions(){
667
+ const apiKey = $('api-key').value.trim();
668
+ if (!apiKey) {
669
+ $('admin-sessions-result').textContent = '{"error": "Please enter API key"}';
670
+ return;
671
+ }
672
+ $('admin-sessions-result').textContent = 'Loading...';
673
+ const res = await fetch(buildUrl('/admin/sessions'), {
674
+ headers: { 'Authorization': 'Bearer ' + apiKey }
675
+ });
676
+ const data = await res.json();
677
+ $('admin-sessions-result').textContent = JSON.stringify(data, null, 2);
678
+ }
679
+
583
680
  $('prompt').addEventListener('input', updatePayloadPreview);
584
681
  $('from').addEventListener('input', updateCurlCommands);
585
682
  $('signature').addEventListener('input', updateCurlCommands);