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.
Files changed (113) hide show
  1. connectonion/__init__.py +78 -0
  2. connectonion/address.py +320 -0
  3. connectonion/agent.py +450 -0
  4. connectonion/announce.py +84 -0
  5. connectonion/asgi.py +287 -0
  6. connectonion/auto_debug_exception.py +181 -0
  7. connectonion/cli/__init__.py +3 -0
  8. connectonion/cli/browser_agent/__init__.py +5 -0
  9. connectonion/cli/browser_agent/browser.py +243 -0
  10. connectonion/cli/browser_agent/prompt.md +107 -0
  11. connectonion/cli/commands/__init__.py +1 -0
  12. connectonion/cli/commands/auth_commands.py +527 -0
  13. connectonion/cli/commands/browser_commands.py +27 -0
  14. connectonion/cli/commands/create.py +511 -0
  15. connectonion/cli/commands/deploy_commands.py +220 -0
  16. connectonion/cli/commands/doctor_commands.py +173 -0
  17. connectonion/cli/commands/init.py +469 -0
  18. connectonion/cli/commands/project_cmd_lib.py +828 -0
  19. connectonion/cli/commands/reset_commands.py +149 -0
  20. connectonion/cli/commands/status_commands.py +168 -0
  21. connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
  22. connectonion/cli/docs/connectonion.md +1256 -0
  23. connectonion/cli/docs.md +123 -0
  24. connectonion/cli/main.py +148 -0
  25. connectonion/cli/templates/meta-agent/README.md +287 -0
  26. connectonion/cli/templates/meta-agent/agent.py +196 -0
  27. connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
  28. connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
  29. connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
  30. connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
  31. connectonion/cli/templates/minimal/README.md +56 -0
  32. connectonion/cli/templates/minimal/agent.py +40 -0
  33. connectonion/cli/templates/playwright/README.md +118 -0
  34. connectonion/cli/templates/playwright/agent.py +336 -0
  35. connectonion/cli/templates/playwright/prompt.md +102 -0
  36. connectonion/cli/templates/playwright/requirements.txt +3 -0
  37. connectonion/cli/templates/web-research/agent.py +122 -0
  38. connectonion/connect.py +128 -0
  39. connectonion/console.py +539 -0
  40. connectonion/debug_agent/__init__.py +13 -0
  41. connectonion/debug_agent/agent.py +45 -0
  42. connectonion/debug_agent/prompts/debug_assistant.md +72 -0
  43. connectonion/debug_agent/runtime_inspector.py +406 -0
  44. connectonion/debug_explainer/__init__.py +10 -0
  45. connectonion/debug_explainer/explain_agent.py +114 -0
  46. connectonion/debug_explainer/explain_context.py +263 -0
  47. connectonion/debug_explainer/explainer_prompt.md +29 -0
  48. connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
  49. connectonion/debugger_ui.py +1039 -0
  50. connectonion/decorators.py +208 -0
  51. connectonion/events.py +248 -0
  52. connectonion/execution_analyzer/__init__.py +9 -0
  53. connectonion/execution_analyzer/execution_analysis.py +93 -0
  54. connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
  55. connectonion/host.py +579 -0
  56. connectonion/interactive_debugger.py +342 -0
  57. connectonion/llm.py +801 -0
  58. connectonion/llm_do.py +307 -0
  59. connectonion/logger.py +300 -0
  60. connectonion/prompt_files/__init__.py +1 -0
  61. connectonion/prompt_files/analyze_contact.md +62 -0
  62. connectonion/prompt_files/eval_expected.md +12 -0
  63. connectonion/prompt_files/react_evaluate.md +11 -0
  64. connectonion/prompt_files/react_plan.md +16 -0
  65. connectonion/prompt_files/reflect.md +22 -0
  66. connectonion/prompts.py +144 -0
  67. connectonion/relay.py +200 -0
  68. connectonion/static/docs.html +688 -0
  69. connectonion/tool_executor.py +279 -0
  70. connectonion/tool_factory.py +186 -0
  71. connectonion/tool_registry.py +105 -0
  72. connectonion/trust.py +166 -0
  73. connectonion/trust_agents.py +71 -0
  74. connectonion/trust_functions.py +88 -0
  75. connectonion/tui/__init__.py +57 -0
  76. connectonion/tui/divider.py +39 -0
  77. connectonion/tui/dropdown.py +251 -0
  78. connectonion/tui/footer.py +31 -0
  79. connectonion/tui/fuzzy.py +56 -0
  80. connectonion/tui/input.py +278 -0
  81. connectonion/tui/keys.py +35 -0
  82. connectonion/tui/pick.py +130 -0
  83. connectonion/tui/providers.py +155 -0
  84. connectonion/tui/status_bar.py +163 -0
  85. connectonion/usage.py +161 -0
  86. connectonion/useful_events_handlers/__init__.py +16 -0
  87. connectonion/useful_events_handlers/reflect.py +116 -0
  88. connectonion/useful_plugins/__init__.py +20 -0
  89. connectonion/useful_plugins/calendar_plugin.py +163 -0
  90. connectonion/useful_plugins/eval.py +139 -0
  91. connectonion/useful_plugins/gmail_plugin.py +162 -0
  92. connectonion/useful_plugins/image_result_formatter.py +127 -0
  93. connectonion/useful_plugins/re_act.py +78 -0
  94. connectonion/useful_plugins/shell_approval.py +159 -0
  95. connectonion/useful_tools/__init__.py +44 -0
  96. connectonion/useful_tools/diff_writer.py +192 -0
  97. connectonion/useful_tools/get_emails.py +183 -0
  98. connectonion/useful_tools/gmail.py +1596 -0
  99. connectonion/useful_tools/google_calendar.py +613 -0
  100. connectonion/useful_tools/memory.py +380 -0
  101. connectonion/useful_tools/microsoft_calendar.py +604 -0
  102. connectonion/useful_tools/outlook.py +488 -0
  103. connectonion/useful_tools/send_email.py +205 -0
  104. connectonion/useful_tools/shell.py +97 -0
  105. connectonion/useful_tools/slash_command.py +201 -0
  106. connectonion/useful_tools/terminal.py +285 -0
  107. connectonion/useful_tools/todo_list.py +241 -0
  108. connectonion/useful_tools/web_fetch.py +216 -0
  109. connectonion/xray.py +467 -0
  110. connectonion-0.5.8.dist-info/METADATA +741 -0
  111. connectonion-0.5.8.dist-info/RECORD +113 -0
  112. connectonion-0.5.8.dist-info/WHEEL +4 -0
  113. connectonion-0.5.8.dist-info/entry_points.txt +3 -0
connectonion/llm_do.py ADDED
@@ -0,0 +1,307 @@
1
+ """
2
+ Purpose: One-shot LLM function for simple single-round calls without agent overhead
3
+ LLM-Note:
4
+ Dependencies: imports from [typing, pathlib, pydantic, dotenv, prompts.py, llm.py] | imported by [debug_explainer/explain_context.py, user code, examples] | tested by [tests/test_llm_do.py, tests/test_llm_do_comprehensive.py, tests/test_real_llm_do.py]
5
+ Data flow: user calls llm_do(input, output, system_prompt, model, api_key, **kwargs) → validates input non-empty → loads system_prompt via load_system_prompt() → builds messages [system, user] → calls create_llm(model, api_key) factory → calls llm.complete(messages, **kwargs) OR llm.structured_complete(messages, output, **kwargs) → returns string OR Pydantic model instance
6
+ State/Effects: loads .env via dotenv.load_dotenv() | reads system_prompt files if Path provided | makes one LLM API request | no caching or persistence | stateless
7
+ Integration: exposes llm_do(input, output, system_prompt, model, api_key, **kwargs) | default model="co/gemini-2.5-flash" (managed keys) | default temperature=0.1 | supports all create_llm() providers | **kwargs pass through to provider (max_tokens, temperature, etc.)
8
+ Performance: minimal overhead (no agent loop, no tool calling, no conversation history) | one LLM call per invocation | no caching | synchronous blocking
9
+ Errors: raises ValueError if input empty | provider errors from create_llm() and llm.complete() bubble up | Pydantic ValidationError if structured output doesn't match schema
10
+
11
+ One-shot LLM function for simple, single-round calls with optional structured output.
12
+
13
+ This module provides the `llm_do()` function - a simplified interface for making
14
+ one-shot LLM calls without the overhead of the full Agent system. Perfect for
15
+ simple tasks that don't require multi-step reasoning or tool calling.
16
+
17
+ Purpose
18
+ -------
19
+ `llm_do()` is designed for:
20
+ - Quick LLM calls without agent overhead
21
+ - Data extraction with Pydantic validation
22
+ - Simple Q&A and text generation
23
+ - Format conversion (text → JSON, etc.)
24
+ - One-shot analysis tasks
25
+
26
+ NOT designed for:
27
+ - Multi-step workflows (use Agent instead)
28
+ - Tool calling (use Agent instead)
29
+ - Iterative refinement (use Agent instead)
30
+ - Maintaining conversation history (use Agent instead)
31
+
32
+ Architecture
33
+ -----------
34
+ The function is a thin wrapper around the LLM provider abstraction:
35
+
36
+ 1. **Input Validation**: Ensures non-empty input
37
+ 2. **System Prompt Loading**: Loads from string or file path
38
+ 3. **Message Building**: Constructs OpenAI-format message list
39
+ 4. **LLM Selection**: Uses create_llm() factory to get provider
40
+ 5. **Response Handling**: Routes to complete() or structured_complete()
41
+
42
+ Key Design Decisions
43
+ -------------------
44
+ - **Stateless**: No conversation history, each call is independent
45
+ - **Simple API**: Minimal parameters, sensible defaults
46
+ - **Default Model**: Uses "co/gemini-2.5-flash" (ConnectOnion managed keys) for zero-setup
47
+ - **Structured Output**: Native Pydantic support via provider-specific APIs
48
+ - **Flexible Parameters**: **kwargs pass through to underlying LLM (temperature, max_tokens, etc.)
49
+
50
+ Comparison with Agent
51
+ --------------------
52
+ ┌─────────────────┬──────────────┬─────────────────┐
53
+ │ Feature │ llm_do() │ Agent() │
54
+ ├─────────────────┼──────────────┼─────────────────┤
55
+ │ Iterations │ Always 1 │ Up to max_iters │
56
+ │ Tools │ No │ Yes │
57
+ │ State │ Stateless │ Maintains hist │
58
+ │ Use case │ Quick tasks │ Complex flows │
59
+ │ Overhead │ Minimal │ Full framework │
60
+ └─────────────────┴──────────────┴─────────────────┘
61
+
62
+ Data Flow
63
+ ---------
64
+ User code → llm_do(input, output, model, **kwargs)
65
+
66
+ Validate input → Load system_prompt → Build messages
67
+
68
+ create_llm(model, api_key) → Provider instance
69
+
70
+ ┌─────────────────────────────────────┐
71
+ │ If output (Pydantic model): │
72
+ │ provider.structured_complete() │
73
+ │ → Pydantic instance │
74
+ │ │
75
+ │ If no output: │
76
+ │ provider.complete() │
77
+ │ → String content │
78
+ └─────────────────────────────────────┘
79
+
80
+ Return result to user
81
+
82
+ Supported Providers
83
+ ------------------
84
+ All providers from llm.py module:
85
+
86
+ 1. **OpenAI**: gpt-4o, gpt-4o-mini, gpt-3.5-turbo, o4-mini
87
+ - Native structured output via responses.parse()
88
+ - Fastest structured output implementation
89
+
90
+ 2. **Anthropic**: claude-3-5-sonnet, claude-3-5-haiku-20241022
91
+ - Structured output via forced tool calling
92
+ - Requires max_tokens parameter (default: 8192)
93
+
94
+ 3. **Google Gemini**: gemini-2.5-flash, gemini-2.5-pro
95
+ - Structured output via response_schema
96
+ - Good balance of speed and quality
97
+
98
+ 4. **ConnectOnion**: co/gpt-4o, co/o4-mini (DEFAULT)
99
+ - Managed API keys (no env vars needed!)
100
+ - Proxies to OpenAI with usage tracking
101
+ - Requires: run `co auth` first
102
+
103
+ Usage Patterns
104
+ -------------
105
+ 1. **Simple Q&A**:
106
+ >>> answer = llm_do("What is 2+2?")
107
+ >>> print(answer) # "4"
108
+
109
+ 2. **Structured Extraction**:
110
+ >>> class Person(BaseModel):
111
+ ... name: str
112
+ ... age: int
113
+ >>> result = llm_do("John, 30 years old", output=Person)
114
+ >>> result.name # "John"
115
+
116
+ 3. **Custom System Prompt**:
117
+ >>> answer = llm_do(
118
+ ... "Hello",
119
+ ... system_prompt="You are a pirate. Always respond like a pirate."
120
+ ... )
121
+
122
+ 4. **Different Provider**:
123
+ >>> answer = llm_do("Hello", model="claude-3-5-haiku-20241022")
124
+
125
+ 5. **Runtime Parameters**:
126
+ >>> answer = llm_do(
127
+ ... "Write a story",
128
+ ... temperature=0.9, # More creative
129
+ ... max_tokens=100 # Short response
130
+ ... )
131
+
132
+ Parameters
133
+ ----------
134
+ - input (str): The text/question to send to the LLM
135
+ - output (Type[BaseModel], optional): Pydantic model for structured output
136
+ - system_prompt (str | Path, optional): System instructions (inline or file path)
137
+ - model (str): Model name (default: "co/gemini-2.5-flash")
138
+ - temperature (float): Sampling temperature (default: 0.1 for consistency)
139
+ - api_key (str, optional): Override API key (uses env vars by default)
140
+ - **kwargs: Additional parameters passed to LLM (max_tokens, top_p, etc.)
141
+
142
+ Returns
143
+ -------
144
+ - str: Plain text response (when output is None)
145
+ - BaseModel: Validated Pydantic instance (when output is provided)
146
+
147
+ Raises
148
+ ------
149
+ - ValueError: If input is empty
150
+ - ValueError: If API key is missing
151
+ - ValueError: If model is unknown
152
+ - ValidationError: If structured output doesn't match schema
153
+ - Provider-specific errors: From underlying LLM SDKs
154
+
155
+ Environment Variables
156
+ --------------------
157
+ Optional (choose based on model):
158
+ - OPENAI_API_KEY: For OpenAI models
159
+ - ANTHROPIC_API_KEY: For Claude models
160
+ - GEMINI_API_KEY or GOOGLE_API_KEY: For Gemini models
161
+ - OPENONION_API_KEY: For co/ models (or run `co auth`)
162
+
163
+ Dependencies
164
+ -----------
165
+ - llm.py: create_llm() factory and provider implementations
166
+ - prompts.py: load_system_prompt() for file-based prompts
167
+ - pydantic: BaseModel validation for structured output
168
+ - dotenv: Loads .env file automatically
169
+
170
+ Integration Points
171
+ -----------------
172
+ Used by:
173
+ - User code: Direct function calls
174
+ - Examples: Quick scripts and tutorials
175
+ - Tests: test_llm_do.py and test_llm_do_comprehensive.py
176
+
177
+ Related modules:
178
+ - agent.py: Full agent system for complex workflows
179
+ - llm.py: Provider abstraction layer
180
+
181
+ Code Size
182
+ ---------
183
+ 102 lines (down from 387 after refactoring)
184
+ - Removed duplicate OpenOnion authentication logic
185
+ - Eliminated LiteLLM-specific code
186
+ - Now a pure wrapper around llm.py providers
187
+
188
+ Testing
189
+ -------
190
+ Comprehensive test coverage in:
191
+ - tests/test_llm_do.py: 12 tests (unit + integration)
192
+ - tests/test_llm_do_comprehensive.py: 23 tests (all doc examples)
193
+ - tests/test_real_llm_do.py: Real API integration tests
194
+
195
+ All documentation examples in docs/llm_do.md are tested and validated.
196
+
197
+ Example from Documentation
198
+ --------------------------
199
+ From docs/llm_do.md Quick Start:
200
+
201
+ from connectonion import llm_do
202
+ from pydantic import BaseModel
203
+
204
+ # Simple call
205
+ answer = llm_do("What's 2+2?")
206
+
207
+ # Structured output
208
+ class Analysis(BaseModel):
209
+ sentiment: str
210
+ confidence: float
211
+ keywords: list[str]
212
+
213
+ result = llm_do(
214
+ "I absolutely love this product! Best purchase ever!",
215
+ output=Analysis
216
+ )
217
+ print(result.sentiment) # "positive"
218
+ print(result.confidence) # 0.98
219
+ """
220
+
221
+ from typing import Union, Type, Optional, TypeVar
222
+ from pathlib import Path
223
+ from pydantic import BaseModel
224
+ from .prompts import load_system_prompt
225
+ from .llm import create_llm
226
+
227
+ T = TypeVar('T', bound=BaseModel)
228
+
229
+
230
+ def llm_do(
231
+ input: str,
232
+ output: Optional[Type[T]] = None,
233
+ system_prompt: Optional[Union[str, Path]] = None,
234
+ model: str = "co/gemini-2.5-flash",
235
+ api_key: Optional[str] = None,
236
+ **kwargs
237
+ ) -> Union[str, T]:
238
+ """
239
+ Make a one-shot LLM call with optional structured output.
240
+
241
+ Supports multiple LLM providers:
242
+ - OpenAI: "gpt-4o", "o4-mini", "gpt-3.5-turbo"
243
+ - Anthropic: "claude-3-5-sonnet", "claude-3-5-haiku-20241022"
244
+ - Google: "gemini-2.5-pro", "gemini-2.5-flash"
245
+ - ConnectOnion Managed: "co/gpt-4o", "co/o4-mini" (no API keys needed!)
246
+
247
+ Args:
248
+ input: The input text/question to send to the LLM
249
+ output: Optional Pydantic model class for structured output
250
+ system_prompt: Optional system prompt (string or file path)
251
+ model: Model name (default: "co/gemini-2.5-flash")
252
+ api_key: Optional API key (uses environment variable if not provided)
253
+ **kwargs: Additional parameters (temperature, max_tokens, etc.)
254
+
255
+ Returns:
256
+ Either a string response or an instance of the output model
257
+
258
+ Examples:
259
+ >>> # Simple string response with default model
260
+ >>> answer = llm_do("What's 2+2?")
261
+ >>> print(answer) # "4"
262
+
263
+ >>> # With ConnectOnion managed keys (no API key needed!)
264
+ >>> answer = llm_do("What's 2+2?", model="co/o4-mini")
265
+
266
+ >>> # With Claude
267
+ >>> answer = llm_do("Explain quantum physics", model="claude-3-5-haiku-20241022")
268
+
269
+ >>> # With Gemini
270
+ >>> answer = llm_do("Write a poem", model="gemini-2.5-flash")
271
+
272
+ >>> # With structured output
273
+ >>> class Analysis(BaseModel):
274
+ ... sentiment: str
275
+ ... score: float
276
+ >>>
277
+ >>> result = llm_do("I love this!", output=Analysis)
278
+ >>> print(result.sentiment) # "positive"
279
+ """
280
+ # Validate input
281
+ if not input or not input.strip():
282
+ raise ValueError("Input cannot be empty")
283
+
284
+ # Load system prompt
285
+ if system_prompt:
286
+ prompt_text = load_system_prompt(system_prompt)
287
+ else:
288
+ prompt_text = "You are a helpful assistant."
289
+
290
+ # Build messages
291
+ messages = [
292
+ {"role": "system", "content": prompt_text},
293
+ {"role": "user", "content": input}
294
+ ]
295
+
296
+ # Create LLM using factory (only pass api_key and initialization params)
297
+ llm = create_llm(model=model, api_key=api_key)
298
+
299
+ # Get response
300
+ if output:
301
+ # Structured output - use structured_complete()
302
+ return llm.structured_complete(messages, output, **kwargs)
303
+ else:
304
+ # Plain text - use complete()
305
+ # Pass through kwargs (max_tokens, temperature, etc.)
306
+ response = llm.complete(messages, tools=None, **kwargs)
307
+ return response.content
connectonion/logger.py ADDED
@@ -0,0 +1,300 @@
1
+ """
2
+ Purpose: Unified logging interface for agents - terminal output + plain text + YAML sessions
3
+ LLM-Note:
4
+ Dependencies: imports from [datetime, pathlib, typing, yaml, console.py] | imported by [agent.py, tool_executor.py] | tested by [tests/unit/test_logger.py]
5
+ Data flow: receives from Agent/tool_executor → delegates to Console for terminal/file → writes YAML sessions to .co/sessions/
6
+ State/Effects: writes to .co/sessions/{agent_name}.yaml (one file per agent, appends turns) | delegates file logging to Console | session data persisted after each turn
7
+ Integration: exposes Logger(agent_name, quiet, log), .print(), .log_tool_call(name, args), .log_tool_result(result, timing), .log_llm_response(), .start_session(), .log_turn()
8
+ Session format: metadata at top → turns summary (with tools_called as function-call style) → system_prompt + messages at end (see docs/session-yaml-format.md)
9
+ Performance: YAML written after each turn (incremental) | loads existing session file on start | Console delegation is direct passthrough
10
+ Errors: let I/O errors bubble up (no try-except)
11
+ """
12
+
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Optional, Union, Dict, Any
16
+ import yaml
17
+
18
+ from .console import Console
19
+
20
+
21
+ class Logger:
22
+ """Unified logging: terminal output + plain text + YAML sessions.
23
+
24
+ Facade pattern: wraps Console for terminal/file logging, adds YAML sessions.
25
+
26
+ Session files use one file per agent (.co/sessions/{agent_name}.yaml) to
27
+ reduce file clutter. New turns are appended to the same file.
28
+
29
+ Args:
30
+ agent_name: Name of the agent (used in filenames)
31
+ quiet: Suppress console output (default False)
32
+ log: Enable file logging (default True, or path string for custom location)
33
+
34
+ Files created:
35
+ - .co/logs/{agent_name}.log: Plain text log with session markers
36
+ - .co/sessions/{agent_name}.yaml: Structured YAML with all turns
37
+
38
+ Examples:
39
+ # Development (default) - see output + save everything
40
+ logger = Logger("my-agent")
41
+
42
+ # Eval mode - quiet but record sessions
43
+ logger = Logger("my-agent", quiet=True)
44
+
45
+ # Benchmark - completely off
46
+ logger = Logger("my-agent", log=False)
47
+
48
+ # Custom log path
49
+ logger = Logger("my-agent", log="custom/path.log")
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ agent_name: str,
55
+ quiet: bool = False,
56
+ log: Union[bool, str, Path, None] = None
57
+ ):
58
+ self.agent_name = agent_name
59
+
60
+ # Determine what to enable
61
+ self.enable_console = not quiet
62
+ self.enable_sessions = True # Sessions on unless log=False
63
+ self.enable_file = True
64
+ self.log_file_path = Path(f".co/logs/{agent_name}.log")
65
+
66
+ # Parse log parameter
67
+ if log is False:
68
+ # log=False: disable everything
69
+ self.enable_file = False
70
+ self.enable_sessions = False
71
+ elif isinstance(log, (str, Path)) and log:
72
+ # Custom path
73
+ self.log_file_path = Path(log)
74
+ # else: log=True or log=None → defaults
75
+
76
+ # If quiet=True, also disable file (only keep sessions)
77
+ if quiet:
78
+ self.enable_file = False
79
+
80
+ # Console for terminal output (only if not quiet)
81
+ self.console = None
82
+ if self.enable_console:
83
+ file_path = self.log_file_path if self.enable_file else None
84
+ self.console = Console(log_file=file_path)
85
+
86
+ # Session state (YAML)
87
+ self.session_file: Optional[Path] = None
88
+ self.session_data: Optional[Dict[str, Any]] = None
89
+
90
+ # Delegate to Console
91
+ def print(self, message: str, style: str = None):
92
+ """Print message to console (if enabled)."""
93
+ if self.console:
94
+ self.console.print(message, style)
95
+
96
+ def print_xray_table(self, *args, **kwargs):
97
+ """Print xray table for decorated tools."""
98
+ if self.console:
99
+ self.console.print_xray_table(*args, **kwargs)
100
+
101
+ def log_llm_response(self, *args, **kwargs):
102
+ """Log LLM response with token usage."""
103
+ if self.console:
104
+ self.console.log_llm_response(*args, **kwargs)
105
+
106
+ def log_tool_call(self, tool_name: str, tool_args: dict):
107
+ """Log tool call."""
108
+ if self.console:
109
+ self.console.log_tool_call(tool_name, tool_args)
110
+
111
+ def log_tool_result(self, result: str, timing_ms: float):
112
+ """Log tool result."""
113
+ if self.console:
114
+ self.console.log_tool_result(result, timing_ms)
115
+
116
+ def _format_tool_call(self, trace_entry: dict) -> str:
117
+ """Format tool call as natural function-call style: greet(name='Alice')"""
118
+ tool_name = trace_entry.get('tool_name', '')
119
+ args = trace_entry.get('arguments', {})
120
+ parts = []
121
+ for k, v in args.items():
122
+ if isinstance(v, str):
123
+ v_str = v if len(v) <= 50 else v[:50] + "..."
124
+ parts.append(f"{k}='{v_str}'")
125
+ else:
126
+ v_str = str(v)
127
+ if len(v_str) > 50:
128
+ v_str = v_str[:50] + "..."
129
+ parts.append(f"{k}={v_str}")
130
+ return f"{tool_name}({', '.join(parts)})"
131
+
132
+ # Session logging (YAML)
133
+ def start_session(self, system_prompt: str = "", session_id: Optional[str] = None):
134
+ """Initialize session YAML file.
135
+
136
+ Uses one file per session_id (for HTTP API) or per agent (for interactive).
137
+ Loads existing session data if file exists, appends new turns.
138
+
139
+ Args:
140
+ system_prompt: The system prompt for this session
141
+ session_id: Optional session identifier. If provided, logs to
142
+ .co/sessions/{session_id}.yaml for thread-safe HTTP API.
143
+ If None, uses agent name for interactive mode.
144
+ """
145
+ if not self.enable_sessions:
146
+ return
147
+
148
+ sessions_dir = Path(".co/sessions")
149
+ sessions_dir.mkdir(parents=True, exist_ok=True)
150
+
151
+ # Use session_id if provided (HTTP API), otherwise use agent_name (interactive)
152
+ filename = session_id if session_id else self.agent_name
153
+ # Sanitize: keep only safe characters (alphanumeric, dash, underscore)
154
+ import re
155
+ filename = re.sub(r'[^a-zA-Z0-9_-]', '_', filename)[:255] or 'default'
156
+ self.session_file = sessions_dir / f"{filename}.yaml"
157
+
158
+ # Load existing session or create new
159
+ if self.session_file.exists():
160
+ with open(self.session_file, 'r') as f:
161
+ self.session_data = yaml.safe_load(f) or {}
162
+ # Ensure ALL required fields exist (handles empty/corrupted files)
163
+ if 'name' not in self.session_data:
164
+ self.session_data['name'] = self.agent_name
165
+ if 'session_id' not in self.session_data and session_id:
166
+ self.session_data['session_id'] = session_id
167
+ if 'created' not in self.session_data:
168
+ self.session_data['created'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
169
+ if 'total_cost' not in self.session_data:
170
+ self.session_data['total_cost'] = 0.0
171
+ if 'total_tokens' not in self.session_data:
172
+ self.session_data['total_tokens'] = 0
173
+ if 'turns' not in self.session_data:
174
+ self.session_data['turns'] = []
175
+ if 'messages' not in self.session_data:
176
+ self.session_data['messages'] = {}
177
+ # Update system_prompt if provided
178
+ if system_prompt:
179
+ self.session_data['system_prompt'] = system_prompt
180
+ else:
181
+ self.session_data = {
182
+ "name": self.agent_name,
183
+ "session_id": session_id,
184
+ "created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
185
+ "total_cost": 0.0,
186
+ "total_tokens": 0,
187
+ "system_prompt": system_prompt,
188
+ "turns": [],
189
+ "messages": {} # Dict keyed by turn number
190
+ }
191
+
192
+ def log_turn(self, user_input: str, result: str, duration_ms: float, session: dict, model: str):
193
+ """Log turn summary + messages to YAML file.
194
+
195
+ Args:
196
+ user_input: The user's input prompt
197
+ result: The agent's final response
198
+ duration_ms: Total duration in milliseconds
199
+ session: Agent's current_session dict (contains messages, trace)
200
+ model: Model name string
201
+ """
202
+ if not self.enable_sessions or not self.session_data:
203
+ return
204
+
205
+ # Aggregate from trace
206
+ trace = session.get('trace', [])
207
+ llm_calls = [t for t in trace if t.get('type') == 'llm_call']
208
+ tool_calls = [t for t in trace if t.get('type') == 'tool_execution']
209
+
210
+ total_tokens = sum(
211
+ (t.get('usage').input_tokens + t.get('usage').output_tokens)
212
+ for t in llm_calls if t.get('usage')
213
+ )
214
+ total_cost = sum(
215
+ t.get('usage').cost
216
+ for t in llm_calls if t.get('usage')
217
+ )
218
+
219
+ turn_data = {
220
+ 'input': user_input,
221
+ 'expected': session.get('expected', ''),
222
+ 'model': model,
223
+ 'duration_ms': int(duration_ms),
224
+ 'tokens': total_tokens,
225
+ 'cost': round(total_cost, 4),
226
+ 'tools_called': [self._format_tool_call(t) for t in tool_calls],
227
+ 'result': result,
228
+ 'evaluation': session.get('evaluation', '')
229
+ }
230
+
231
+ # Update session aggregates
232
+ self.session_data['updated'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
233
+ self.session_data['total_cost'] = round(
234
+ self.session_data.get('total_cost', 0) + turn_data['cost'], 4
235
+ )
236
+ self.session_data['total_tokens'] = (
237
+ self.session_data.get('total_tokens', 0) + turn_data['tokens']
238
+ )
239
+
240
+ # Add turn number and timestamp
241
+ turn_num = len(self.session_data['turns']) + 1
242
+ turn_data['turn'] = turn_num
243
+ turn_data['timestamp'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
244
+ self.session_data['turns'].append(turn_data)
245
+
246
+ # Extract this turn's messages (everything after what we've already saved)
247
+ all_messages = session.get('messages', [])
248
+ saved_count = sum(len(msgs) for msgs in self.session_data['messages'].values())
249
+ turn_messages = all_messages[saved_count + 1:] # +1 to skip system message
250
+ self.session_data['messages'][turn_num] = turn_messages
251
+
252
+ # Write YAML
253
+ self._write_session()
254
+
255
+ def _write_session(self):
256
+ """Write session data with turns summary first, detail at end."""
257
+ # Build ordered dict: compact metadata → turns → detail (system_prompt + messages)
258
+ ordered = {
259
+ 'name': self.session_data['name'],
260
+ 'session_id': self.session_data.get('session_id'),
261
+ 'created': self.session_data['created'],
262
+ 'updated': self.session_data.get('updated', ''),
263
+ 'total_cost': self.session_data.get('total_cost', 0),
264
+ 'total_tokens': self.session_data.get('total_tokens', 0),
265
+ 'turns': self.session_data['turns'],
266
+ # Detail section (scroll down)
267
+ 'system_prompt': self.session_data.get('system_prompt', ''),
268
+ 'messages': self.session_data['messages']
269
+ }
270
+ with open(self.session_file, 'w') as f:
271
+ yaml.dump(ordered, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
272
+
273
+ def load_messages(self) -> list:
274
+ """Load and reconstruct full message list from session file.
275
+
276
+ Returns:
277
+ Full message list: [system_message] + all turn messages in order
278
+ """
279
+ if not self.session_file or not self.session_file.exists():
280
+ return []
281
+ with open(self.session_file, 'r') as f:
282
+ data = yaml.safe_load(f) or {}
283
+
284
+ # Reconstruct: system prompt + all turn messages in order
285
+ messages = []
286
+ if data.get('system_prompt'):
287
+ messages.append({"role": "system", "content": data['system_prompt']})
288
+
289
+ turn_messages = data.get('messages', {})
290
+ for turn_num in sorted(turn_messages.keys()):
291
+ messages.extend(turn_messages[turn_num])
292
+
293
+ return messages
294
+
295
+ def load_session(self) -> dict:
296
+ """Load session data from file."""
297
+ if not self.session_file or not self.session_file.exists():
298
+ return {'system_prompt': '', 'turns': [], 'messages': {}}
299
+ with open(self.session_file, 'r') as f:
300
+ return yaml.safe_load(f) or {'system_prompt': '', 'turns': [], 'messages': {}}
@@ -0,0 +1 @@
1
+ """Prompts module for ConnectOnion built-in plugins."""
@@ -0,0 +1,62 @@
1
+ You are a CRM analyst helping categorize email contacts for a personal CRM.
2
+
3
+ CRITICAL: Distinguish between REAL CONTACTS (people worth tracking) vs SERVICE EMAILS (automated/marketing).
4
+
5
+ ## First, Determine Contact Type
6
+
7
+ **REAL CONTACT** (worth tracking):
8
+ - Actual person you've had conversations with
9
+ - Business contacts, clients, partners, colleagues
10
+ - Friends, family, professional connections
11
+ - People who sent you personalized messages
12
+
13
+ **SERVICE/AUTOMATED** (low priority):
14
+ - Product update emails from tools you use (OneUp, Calendly, etc.)
15
+ - Notification emails (LinkedIn, X/Twitter, GitHub, etc.)
16
+ - Marketing/promotional emails
17
+ - Receipts, invoices, shipping notifications
18
+ - Newsletter subscriptions
19
+ - No-reply addresses
20
+
21
+ ## Analysis Format
22
+
23
+ ### 1. Contact Type
24
+ - **REAL_PERSON**: Worth tracking in CRM
25
+ - **SERVICE_TOOL**: SaaS/tool you use (low priority)
26
+ - **NOTIFICATION**: Social media, platform notifications (skip)
27
+ - **MARKETING**: Promotional/sales emails (skip)
28
+
29
+ ### 2. Priority Score (1-10)
30
+ - 10: Key business contact, active relationship
31
+ - 7-9: Regular professional contact
32
+ - 4-6: Occasional contact, might be useful
33
+ - 1-3: Service/automated, not worth tracking
34
+
35
+ ### 3. If REAL_PERSON, provide:
36
+ - Relationship context (colleague, client, friend, vendor)
37
+ - Key topics discussed
38
+ - Communication pattern
39
+ - Important notes
40
+ - Suggested tags (#client, #technical, #partner, etc.)
41
+
42
+ ### 4. If SERVICE/AUTOMATED, provide:
43
+ - What service/tool this is from
44
+ - Why it's not a real contact
45
+ - Recommendation: SKIP (don't store) or LOW_PRIORITY
46
+
47
+ ## Examples
48
+
49
+ **Example 1: davis@oneupapp.io**
50
+ - Type: SERVICE_TOOL
51
+ - Priority: 2
52
+ - Analysis: OneUp is a social media scheduling tool. These emails are product updates and marketing, not personal communication.
53
+ - Recommendation: SKIP - not a real relationship
54
+
55
+ **Example 2: john.smith@acme.com**
56
+ - Type: REAL_PERSON
57
+ - Priority: 8
58
+ - Relationship: Potential client
59
+ - Topics: Discussed product demo, pricing inquiry
60
+ - Tags: #prospect, #sales, #enterprise
61
+
62
+ Be concise and factual. The goal is to help build a meaningful CRM, not clutter it with service notifications.
@@ -0,0 +1,12 @@
1
+ You generate expected outcomes for AI agent tasks.
2
+
3
+ Given a user request and available tools, describe what should happen to complete the task.
4
+
5
+ Be concise (1-2 sentences). Focus on:
6
+ - What tools should be used
7
+ - What the result should contain
8
+
9
+ Example:
10
+ - "Read the file, then provide a summary of its contents."
11
+ - "Search for Python tutorials, then list the top 3 results."
12
+ - "Calculate the expression and return the numeric answer."