elidia-agent-cli 0.1.0__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.
- elidia/__init__.py +0 -0
- elidia/__main__.py +4 -0
- elidia/agent/__init__.py +3 -0
- elidia/agent/loop.py +449 -0
- elidia/agent/personas.py +130 -0
- elidia/agent/portal.py +115 -0
- elidia/api/__init__.py +0 -0
- elidia/api/client.py +221 -0
- elidia/api/streaming.py +55 -0
- elidia/auth/__init__.py +0 -0
- elidia/auth/keychain.py +86 -0
- elidia/cache/__init__.py +1 -0
- elidia/cache/lru.py +158 -0
- elidia/cli/__init__.py +0 -0
- elidia/cli/commands.py +122 -0
- elidia/cli/main.py +293 -0
- elidia/cli/pager.py +104 -0
- elidia/cli/progress.py +173 -0
- elidia/cli/renderer.py +32 -0
- elidia/cli/repl.py +1234 -0
- elidia/cli/themes.py +214 -0
- elidia/config/__init__.py +0 -0
- elidia/config/defaults.py +5 -0
- elidia/config/rules.py +48 -0
- elidia/config/settings.py +217 -0
- elidia/creative/__init__.py +1 -0
- elidia/creative/audio.py +213 -0
- elidia/creative/display.py +260 -0
- elidia/creative/image.py +142 -0
- elidia/creative/local.py +163 -0
- elidia/creative/video.py +181 -0
- elidia/daemon/__init__.py +0 -0
- elidia/daemon/manager.py +349 -0
- elidia/db/__init__.py +0 -0
- elidia/db/database.py +87 -0
- elidia/mcp/__init__.py +0 -0
- elidia/mcp/client.py +198 -0
- elidia/mcp/config.py +93 -0
- elidia/mcp/registry.py +101 -0
- elidia/mcp/types.py +43 -0
- elidia/memory/__init__.py +3 -0
- elidia/memory/auto.py +164 -0
- elidia/memory/compaction.py +101 -0
- elidia/memory/embeddings.py +47 -0
- elidia/memory/outcomes.py +131 -0
- elidia/memory/patterns.py +79 -0
- elidia/memory/store.py +338 -0
- elidia/models/__init__.py +0 -0
- elidia/models/adaptive.py +53 -0
- elidia/models/router.py +115 -0
- elidia/modes/__init__.py +1 -0
- elidia/modes/autonomous.py +318 -0
- elidia/modes/budget.py +182 -0
- elidia/modes/classifier.py +169 -0
- elidia/modes/consensus.py +210 -0
- elidia/modes/deep_think.py +190 -0
- elidia/modes/swarm.py +229 -0
- elidia/modes/thinking.py +153 -0
- elidia/permissions/__init__.py +0 -0
- elidia/permissions/audit.py +138 -0
- elidia/permissions/manager.py +199 -0
- elidia/permissions/trust.py +63 -0
- elidia/rag/__init__.py +4 -0
- elidia/rag/chunker.py +216 -0
- elidia/rag/engine.py +345 -0
- elidia/rag/ingest.py +154 -0
- elidia/rag/portal_bridge.py +84 -0
- elidia/rag/watcher.py +98 -0
- elidia/research/__init__.py +1 -0
- elidia/research/export.py +237 -0
- elidia/research/orchestrator.py +310 -0
- elidia/research/sources.py +223 -0
- elidia/session/__init__.py +0 -0
- elidia/session/history.py +119 -0
- elidia/session/manager.py +82 -0
- elidia/tools/__init__.py +28 -0
- elidia/tools/base.py +70 -0
- elidia/tools/fetch.py +100 -0
- elidia/tools/filesystem.py +204 -0
- elidia/tools/git.py +156 -0
- elidia/tools/search.py +99 -0
- elidia/tools/terminal.py +77 -0
- elidia/widgets/__init__.py +1 -0
- elidia/widgets/protocol.py +248 -0
- elidia/widgets/renderer.py +225 -0
- elidia/workflow/__init__.py +1 -0
- elidia/workflow/engine.py +377 -0
- elidia_agent_cli-0.1.0.dist-info/METADATA +344 -0
- elidia_agent_cli-0.1.0.dist-info/RECORD +93 -0
- elidia_agent_cli-0.1.0.dist-info/WHEEL +5 -0
- elidia_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- elidia_agent_cli-0.1.0.dist-info/licenses/LICENSE +57 -0
- elidia_agent_cli-0.1.0.dist-info/top_level.txt +1 -0
elidia/__init__.py
ADDED
|
File without changes
|
elidia/__main__.py
ADDED
elidia/agent/__init__.py
ADDED
elidia/agent/loop.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from elidia.api.client import AiUtilsClient, ChatMessage
|
|
8
|
+
from elidia.mcp.registry import MCPRegistry
|
|
9
|
+
from elidia.models.router import ModelRouter
|
|
10
|
+
from elidia.modes.budget import BudgetGovernor
|
|
11
|
+
from elidia.modes.classifier import ExecMode, classify_mode
|
|
12
|
+
from elidia.modes.consensus import run_consensus
|
|
13
|
+
from elidia.modes.deep_think import deep_think_stream, select_reasoning_model
|
|
14
|
+
from elidia.modes.thinking import ThinkingLevel, get_caps
|
|
15
|
+
from elidia.permissions.audit import AuditLogger
|
|
16
|
+
from elidia.permissions.manager import PermissionManager
|
|
17
|
+
from elidia.tools.base import ToolRegistry
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
MAX_TOOL_LOOPS = 25
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AgentEvent:
|
|
26
|
+
kind: str # "thinking", "content", "tool_call", "tool_result", "usage", "error", "done", "mode_info", "budget_warning"
|
|
27
|
+
data: Any = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AgentState:
|
|
32
|
+
messages: list[ChatMessage] = field(default_factory=list)
|
|
33
|
+
model: str = ""
|
|
34
|
+
mode: str = "chat"
|
|
35
|
+
exec_mode: ExecMode = ExecMode.DIRECT
|
|
36
|
+
thinking_level: ThinkingLevel = ThinkingLevel.MEDIUM
|
|
37
|
+
loop_count: int = 0
|
|
38
|
+
tokens_in: int = 0
|
|
39
|
+
tokens_out: int = 0
|
|
40
|
+
total_cost_dt: float = 0.0
|
|
41
|
+
tools_called: list[str] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AgentLoop:
|
|
45
|
+
"""Core agent loop: route → act → observe → reflect."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
client: AiUtilsClient,
|
|
50
|
+
tool_registry: ToolRegistry,
|
|
51
|
+
mcp_registry: MCPRegistry | None = None,
|
|
52
|
+
model_router: ModelRouter | None = None,
|
|
53
|
+
permission_manager: PermissionManager | None = None,
|
|
54
|
+
audit: AuditLogger | None = None,
|
|
55
|
+
budget: BudgetGovernor | None = None,
|
|
56
|
+
max_loops: int = MAX_TOOL_LOOPS,
|
|
57
|
+
thinking_level: ThinkingLevel | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
logger.debug("Entered into AgentLoop.__init__")
|
|
60
|
+
self._client = client
|
|
61
|
+
self._tools = tool_registry
|
|
62
|
+
self._mcp = mcp_registry
|
|
63
|
+
self._router = model_router or ModelRouter()
|
|
64
|
+
self._permissions = permission_manager
|
|
65
|
+
self._audit = audit
|
|
66
|
+
self._budget = budget or BudgetGovernor()
|
|
67
|
+
self._max_loops = max_loops
|
|
68
|
+
self._thinking_level = thinking_level or ThinkingLevel.MEDIUM
|
|
69
|
+
|
|
70
|
+
async def run(
|
|
71
|
+
self,
|
|
72
|
+
messages: list[ChatMessage],
|
|
73
|
+
mode: str = "chat",
|
|
74
|
+
forced_model: str | None = None,
|
|
75
|
+
session_id: str = "",
|
|
76
|
+
thinking_level: ThinkingLevel | None = None,
|
|
77
|
+
) -> AsyncIterator[AgentEvent]:
|
|
78
|
+
logger.debug(f"Entered into AgentLoop.run: mode={mode}, msg_count={len(messages)}")
|
|
79
|
+
|
|
80
|
+
state = AgentState(messages=list(messages), mode=mode)
|
|
81
|
+
state.thinking_level = thinking_level or self._thinking_level
|
|
82
|
+
caps = get_caps(state.thinking_level)
|
|
83
|
+
|
|
84
|
+
user_text = messages[-1].content if messages else ""
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
mode_decision = await classify_mode(self._client, user_text)
|
|
88
|
+
state.exec_mode = mode_decision.mode
|
|
89
|
+
yield AgentEvent(kind="mode_info", data={
|
|
90
|
+
"exec_mode": state.exec_mode.value,
|
|
91
|
+
"cost_label": mode_decision.cost_label,
|
|
92
|
+
"reason": mode_decision.reason,
|
|
93
|
+
})
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.warning(f"Mode classification failed, defaulting to DIRECT: {e}")
|
|
96
|
+
state.exec_mode = ExecMode.DIRECT
|
|
97
|
+
|
|
98
|
+
decision = self._router.route(user_text, mode=mode)
|
|
99
|
+
state.model = forced_model or decision.model
|
|
100
|
+
yield AgentEvent(kind="thinking", data={"model": state.model, "reason": decision.reason})
|
|
101
|
+
|
|
102
|
+
if state.exec_mode == ExecMode.DEEP:
|
|
103
|
+
async for event in self._run_deep_think(state, user_text, session_id):
|
|
104
|
+
yield event
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
if state.exec_mode == ExecMode.CONSENSUS:
|
|
108
|
+
async for event in self._run_consensus(state, user_text, session_id):
|
|
109
|
+
yield event
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
effective_max_loops = min(self._max_loops, caps.max_loops)
|
|
113
|
+
all_tool_schemas = self._build_tool_schemas() if caps.allow_tools else []
|
|
114
|
+
|
|
115
|
+
while state.loop_count <= effective_max_loops:
|
|
116
|
+
state.loop_count += 1
|
|
117
|
+
logger.debug(f"Agent loop iteration {state.loop_count}, model={state.model}")
|
|
118
|
+
|
|
119
|
+
api_messages = self._build_api_messages(state)
|
|
120
|
+
|
|
121
|
+
request_payload: dict[str, Any] = {
|
|
122
|
+
"model": state.model,
|
|
123
|
+
"messages": api_messages,
|
|
124
|
+
"stream": False,
|
|
125
|
+
"temperature": 0.3 if mode == "code" else 0.7,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if all_tool_schemas:
|
|
129
|
+
request_payload["tools"] = all_tool_schemas
|
|
130
|
+
request_payload["tool_choice"] = "auto"
|
|
131
|
+
|
|
132
|
+
if self._budget:
|
|
133
|
+
est_input = sum(len(m.content) // 4 for m in state.messages)
|
|
134
|
+
allowed, cost_est = self._budget.check_and_allow(state.model, est_input)
|
|
135
|
+
if not allowed:
|
|
136
|
+
yield AgentEvent(kind="budget_warning", data={
|
|
137
|
+
"message": cost_est.warning_message,
|
|
138
|
+
"estimated_dt": cost_est.estimated_dt,
|
|
139
|
+
})
|
|
140
|
+
cheaper = self._budget.suggest_cheaper_model(state.model)
|
|
141
|
+
if cheaper:
|
|
142
|
+
state.model = cheaper
|
|
143
|
+
yield AgentEvent(kind="thinking", data={
|
|
144
|
+
"model": state.model,
|
|
145
|
+
"reason": "Budget: switched to cheaper model",
|
|
146
|
+
})
|
|
147
|
+
else:
|
|
148
|
+
yield AgentEvent(kind="error", data=f"Budget exceeded: {cost_est.warning_message}")
|
|
149
|
+
return
|
|
150
|
+
elif cost_est.warning_message:
|
|
151
|
+
yield AgentEvent(kind="budget_warning", data={
|
|
152
|
+
"message": cost_est.warning_message,
|
|
153
|
+
"estimated_dt": cost_est.estimated_dt,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
response = await self._client.chat_completion(
|
|
158
|
+
messages=state.messages,
|
|
159
|
+
model=state.model,
|
|
160
|
+
temperature=request_payload.get("temperature", 0.7),
|
|
161
|
+
)
|
|
162
|
+
except RuntimeError as e:
|
|
163
|
+
yield AgentEvent(kind="error", data=str(e))
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
state.tokens_in += response.tokens_in
|
|
167
|
+
state.tokens_out += response.tokens_out
|
|
168
|
+
cost = response.tokens_in * 0.001 + response.tokens_out * 0.002
|
|
169
|
+
state.total_cost_dt += cost
|
|
170
|
+
|
|
171
|
+
if self._budget:
|
|
172
|
+
self._budget.record_usage(response.tokens_in, response.tokens_out, cost)
|
|
173
|
+
|
|
174
|
+
yield AgentEvent(kind="usage", data={
|
|
175
|
+
"tokens_in": response.tokens_in,
|
|
176
|
+
"tokens_out": response.tokens_out,
|
|
177
|
+
"cost_dt": cost,
|
|
178
|
+
"elapsed_ms": response.elapsed_ms,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
tool_calls = self._extract_tool_calls(response.content)
|
|
182
|
+
|
|
183
|
+
if not tool_calls:
|
|
184
|
+
if response.content:
|
|
185
|
+
yield AgentEvent(kind="content", data=response.content)
|
|
186
|
+
state.messages.append(ChatMessage(role="assistant", content=response.content))
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
state.messages.append(ChatMessage(role="assistant", content=response.content))
|
|
190
|
+
|
|
191
|
+
for tc in tool_calls:
|
|
192
|
+
tool_name = tc["name"]
|
|
193
|
+
tool_args = tc.get("arguments", {})
|
|
194
|
+
call_id = tc.get("id", tool_name)
|
|
195
|
+
|
|
196
|
+
yield AgentEvent(kind="tool_call", data={"name": tool_name, "arguments": tool_args, "id": call_id})
|
|
197
|
+
|
|
198
|
+
if self._permissions:
|
|
199
|
+
action_type = self._classify_tool_action(tool_name, tool_args)
|
|
200
|
+
allowed = self._permissions.check(
|
|
201
|
+
action=action_type,
|
|
202
|
+
session_id=session_id,
|
|
203
|
+
path=tool_args.get("path"),
|
|
204
|
+
command=tool_args.get("command"),
|
|
205
|
+
description=f"Tool call: {tool_name}({json.dumps(tool_args, default=str)[:200]})",
|
|
206
|
+
)
|
|
207
|
+
if not allowed:
|
|
208
|
+
result_text = f"Permission denied for {tool_name}"
|
|
209
|
+
yield AgentEvent(kind="tool_result", data={"name": tool_name, "content": result_text, "is_error": True})
|
|
210
|
+
state.messages.append(ChatMessage(role="user", content=f"[Tool result for {tool_name}]: {result_text}"))
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
result_text, is_error = await self._execute_tool(tool_name, tool_args)
|
|
214
|
+
state.tools_called.append(tool_name)
|
|
215
|
+
|
|
216
|
+
yield AgentEvent(kind="tool_result", data={"name": tool_name, "content": result_text[:2000], "is_error": is_error})
|
|
217
|
+
|
|
218
|
+
if self._audit:
|
|
219
|
+
self._audit.log_tool_call(
|
|
220
|
+
tool_name=tool_name,
|
|
221
|
+
arguments=tool_args,
|
|
222
|
+
result_preview=result_text[:500],
|
|
223
|
+
session_id=session_id,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
state.messages.append(ChatMessage(
|
|
227
|
+
role="user",
|
|
228
|
+
content=f"[Tool result for {tool_name}]:\n{result_text[:8000]}",
|
|
229
|
+
))
|
|
230
|
+
|
|
231
|
+
else:
|
|
232
|
+
yield AgentEvent(kind="error", data=f"Agent exceeded maximum tool loop count ({self._max_loops})")
|
|
233
|
+
|
|
234
|
+
yield AgentEvent(kind="done", data={
|
|
235
|
+
"loops": state.loop_count,
|
|
236
|
+
"tools_called": state.tools_called,
|
|
237
|
+
"total_tokens_in": state.tokens_in,
|
|
238
|
+
"total_tokens_out": state.tokens_out,
|
|
239
|
+
"total_cost_dt": state.total_cost_dt,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
def _build_tool_schemas(self) -> list[dict[str, Any]]:
|
|
243
|
+
logger.debug("Entered into _build_tool_schemas")
|
|
244
|
+
schemas = self._tools.get_schemas_for_llm()
|
|
245
|
+
if self._mcp:
|
|
246
|
+
schemas.extend(self._mcp.get_tool_schemas_for_llm())
|
|
247
|
+
return schemas
|
|
248
|
+
|
|
249
|
+
def _build_api_messages(self, state: AgentState) -> list[dict[str, str]]:
|
|
250
|
+
logger.debug("Entered into _build_api_messages")
|
|
251
|
+
system_prompt = self._get_system_prompt(state.mode)
|
|
252
|
+
result: list[dict[str, str]] = [{"role": "system", "content": system_prompt}]
|
|
253
|
+
for msg in state.messages:
|
|
254
|
+
result.append({"role": msg.role, "content": msg.content})
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
def _get_system_prompt(self, mode: str) -> str:
|
|
258
|
+
logger.debug(f"Entered into _get_system_prompt: mode={mode}")
|
|
259
|
+
|
|
260
|
+
tool_names = [t.name for t in self._tools.list_tools()]
|
|
261
|
+
if self._mcp:
|
|
262
|
+
tool_names.extend(t.name for t in self._mcp.list_all_tools())
|
|
263
|
+
|
|
264
|
+
tool_list = ", ".join(tool_names) if tool_names else "none"
|
|
265
|
+
|
|
266
|
+
base = (
|
|
267
|
+
"You are Elidia, an AI coding agent. You help users with software engineering tasks "
|
|
268
|
+
"by reading files, writing code, running commands, and searching the web.\n\n"
|
|
269
|
+
f"Available tools: {tool_list}\n\n"
|
|
270
|
+
"To use a tool, respond with a JSON block:\n"
|
|
271
|
+
"```tool\n"
|
|
272
|
+
'{"name": "tool_name", "arguments": {"arg1": "value1"}}\n'
|
|
273
|
+
"```\n\n"
|
|
274
|
+
"You can call multiple tools by including multiple ```tool blocks.\n"
|
|
275
|
+
"After tool results are returned, continue reasoning and call more tools or give your final answer.\n"
|
|
276
|
+
"When you have enough information, give your final response directly without tool blocks.\n"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
mode_prompts = {
|
|
280
|
+
"code": "Focus on writing clean, correct, production-quality code. Prefer editing existing files over creating new ones.",
|
|
281
|
+
"research": "Focus on thorough research. Search the web, read documentation, and synthesize findings.",
|
|
282
|
+
"think": "Think step by step. Show your reasoning process before arriving at conclusions.",
|
|
283
|
+
"create": "Focus on creative content generation — writing, brainstorming, design.",
|
|
284
|
+
"chat": "Be helpful and conversational. Use tools when needed to answer questions accurately.",
|
|
285
|
+
}
|
|
286
|
+
base += "\n" + mode_prompts.get(mode, mode_prompts["chat"])
|
|
287
|
+
|
|
288
|
+
return base
|
|
289
|
+
|
|
290
|
+
def _extract_tool_calls(self, content: str) -> list[dict[str, Any]]:
|
|
291
|
+
logger.debug("Entered into _extract_tool_calls")
|
|
292
|
+
calls: list[dict[str, Any]] = []
|
|
293
|
+
|
|
294
|
+
import re
|
|
295
|
+
pattern = r"```tool\s*\n(.*?)```"
|
|
296
|
+
matches = re.findall(pattern, content, re.DOTALL)
|
|
297
|
+
|
|
298
|
+
for match in matches:
|
|
299
|
+
try:
|
|
300
|
+
parsed = json.loads(match.strip())
|
|
301
|
+
if isinstance(parsed, dict) and "name" in parsed:
|
|
302
|
+
calls.append(parsed)
|
|
303
|
+
except json.JSONDecodeError:
|
|
304
|
+
logger.warning(f"Failed to parse tool call JSON: {match[:100]}")
|
|
305
|
+
|
|
306
|
+
return calls
|
|
307
|
+
|
|
308
|
+
async def _execute_tool(self, name: str, arguments: dict[str, Any]) -> tuple[str, bool]:
|
|
309
|
+
logger.debug(f"Entered into _execute_tool: name={name}")
|
|
310
|
+
|
|
311
|
+
builtin = self._tools.get(name)
|
|
312
|
+
if builtin:
|
|
313
|
+
result = await self._tools.call(name, arguments)
|
|
314
|
+
return result.content, result.is_error
|
|
315
|
+
|
|
316
|
+
if self._mcp:
|
|
317
|
+
mcp_match = self._mcp.find_tool(name)
|
|
318
|
+
if mcp_match:
|
|
319
|
+
result = await self._mcp.call_tool(name, arguments)
|
|
320
|
+
content = "\n".join(
|
|
321
|
+
item.get("text", str(item)) if isinstance(item, dict) else str(item)
|
|
322
|
+
for item in result.content
|
|
323
|
+
)
|
|
324
|
+
return content, result.is_error
|
|
325
|
+
|
|
326
|
+
return f"Tool '{name}' not found in any registry", True
|
|
327
|
+
|
|
328
|
+
def _classify_tool_action(self, tool_name: str, args: dict[str, Any]) -> str:
|
|
329
|
+
logger.debug(f"Entered into _classify_tool_action: tool_name={tool_name}")
|
|
330
|
+
|
|
331
|
+
action_map = {
|
|
332
|
+
"file_read": "file_read",
|
|
333
|
+
"file_list": "file_read",
|
|
334
|
+
"file_glob": "file_read",
|
|
335
|
+
"file_grep": "file_read",
|
|
336
|
+
"file_write": "file_write",
|
|
337
|
+
"file_edit": "file_write",
|
|
338
|
+
"file_delete": "file_delete",
|
|
339
|
+
"command_exec": "command_exec",
|
|
340
|
+
"git_status": "file_read",
|
|
341
|
+
"git_diff": "file_read",
|
|
342
|
+
"git_log": "file_read",
|
|
343
|
+
"git_branch": "file_read",
|
|
344
|
+
"git_commit": "command_exec",
|
|
345
|
+
"web_search": "web_search",
|
|
346
|
+
"http_fetch": "web_search",
|
|
347
|
+
}
|
|
348
|
+
return action_map.get(tool_name, "mcp_call_session")
|
|
349
|
+
|
|
350
|
+
async def _run_deep_think(
|
|
351
|
+
self, state: AgentState, user_text: str, session_id: str,
|
|
352
|
+
) -> AsyncIterator[AgentEvent]:
|
|
353
|
+
logger.debug(f"Entered into _run_deep_think: model={state.model}")
|
|
354
|
+
reasoning_model = select_reasoning_model(state.model)
|
|
355
|
+
state.model = reasoning_model
|
|
356
|
+
|
|
357
|
+
yield AgentEvent(kind="thinking", data={
|
|
358
|
+
"model": reasoning_model,
|
|
359
|
+
"reason": "Deep think: using reasoning model",
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
full_reasoning = ""
|
|
363
|
+
full_content = ""
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
async for event in deep_think_stream(
|
|
367
|
+
client=self._client,
|
|
368
|
+
messages=state.messages,
|
|
369
|
+
model=reasoning_model,
|
|
370
|
+
):
|
|
371
|
+
if event.kind == "reasoning":
|
|
372
|
+
full_reasoning += event.data
|
|
373
|
+
elif event.kind == "content":
|
|
374
|
+
full_content += event.data
|
|
375
|
+
elif event.kind == "usage":
|
|
376
|
+
state.tokens_in += event.data.get("tokens_in", 0)
|
|
377
|
+
state.tokens_out += event.data.get("tokens_out", 0)
|
|
378
|
+
cost = event.data.get("cost_dt", 0.0)
|
|
379
|
+
state.total_cost_dt += cost
|
|
380
|
+
if self._budget:
|
|
381
|
+
self._budget.record_usage(
|
|
382
|
+
event.data.get("tokens_in", 0),
|
|
383
|
+
event.data.get("tokens_out", 0),
|
|
384
|
+
cost,
|
|
385
|
+
)
|
|
386
|
+
yield AgentEvent(kind="usage", data=event.data)
|
|
387
|
+
elif event.kind == "error":
|
|
388
|
+
yield AgentEvent(kind="error", data=event.data)
|
|
389
|
+
return
|
|
390
|
+
except Exception as e:
|
|
391
|
+
yield AgentEvent(kind="error", data=str(e))
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
output = full_content or full_reasoning
|
|
395
|
+
if output:
|
|
396
|
+
yield AgentEvent(kind="content", data=output)
|
|
397
|
+
|
|
398
|
+
yield AgentEvent(kind="done", data={
|
|
399
|
+
"loops": 1,
|
|
400
|
+
"tools_called": [],
|
|
401
|
+
"total_tokens_in": state.tokens_in,
|
|
402
|
+
"total_tokens_out": state.tokens_out,
|
|
403
|
+
"total_cost_dt": state.total_cost_dt,
|
|
404
|
+
"exec_mode": "deep",
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
async def _run_consensus(
|
|
408
|
+
self, state: AgentState, user_text: str, session_id: str,
|
|
409
|
+
) -> AsyncIterator[AgentEvent]:
|
|
410
|
+
logger.debug("Entered into _run_consensus")
|
|
411
|
+
|
|
412
|
+
yield AgentEvent(kind="thinking", data={
|
|
413
|
+
"model": "consensus",
|
|
414
|
+
"reason": "Running multiple models in parallel for consensus",
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
result = await run_consensus(
|
|
419
|
+
client=self._client,
|
|
420
|
+
messages=[ChatMessage(role=m.role, content=m.content) for m in state.messages],
|
|
421
|
+
)
|
|
422
|
+
except Exception as e:
|
|
423
|
+
yield AgentEvent(kind="error", data=f"Consensus failed: {e}")
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
output_parts = [f"**Consensus Result** (agreement: {result.agreement}, confidence: {result.confidence:.0%})\n"]
|
|
427
|
+
output_parts.append(result.synthesis)
|
|
428
|
+
output_parts.append("\n\n---\n**Individual Responses:**")
|
|
429
|
+
for r in result.responses:
|
|
430
|
+
output_parts.append(f"\n**{r.model}**: {r.content[:500]}")
|
|
431
|
+
|
|
432
|
+
full_output = "\n".join(output_parts)
|
|
433
|
+
yield AgentEvent(kind="content", data=full_output)
|
|
434
|
+
|
|
435
|
+
total_tokens = sum(r.tokens_out for r in result.responses)
|
|
436
|
+
yield AgentEvent(kind="usage", data={
|
|
437
|
+
"tokens_in": 0,
|
|
438
|
+
"tokens_out": total_tokens,
|
|
439
|
+
"cost_dt": 0.0,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
yield AgentEvent(kind="done", data={
|
|
443
|
+
"loops": 1,
|
|
444
|
+
"tools_called": [],
|
|
445
|
+
"total_tokens_in": 0,
|
|
446
|
+
"total_tokens_out": total_tokens,
|
|
447
|
+
"total_cost_dt": 0.0,
|
|
448
|
+
"exec_mode": "consensus",
|
|
449
|
+
})
|
elidia/agent/personas.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class DomainPersona:
|
|
9
|
+
name: str
|
|
10
|
+
slug: str
|
|
11
|
+
system_prompt: str
|
|
12
|
+
greeting: str = ""
|
|
13
|
+
icon: str = ""
|
|
14
|
+
preferred_model: str = ""
|
|
15
|
+
preferred_tools: list[str] = field(default_factory=list)
|
|
16
|
+
temperature: float = 0.7
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
BUILT_IN_PERSONAS: dict[str, DomainPersona] = {
|
|
20
|
+
"coder": DomainPersona(
|
|
21
|
+
name="Coder",
|
|
22
|
+
slug="coder",
|
|
23
|
+
icon=">>",
|
|
24
|
+
system_prompt=(
|
|
25
|
+
"You are an expert software engineer. You write clean, correct, production-quality code. "
|
|
26
|
+
"You read existing code before modifying it. You prefer editing over rewriting. "
|
|
27
|
+
"You use tools to read files, run tests, and verify your work."
|
|
28
|
+
),
|
|
29
|
+
greeting="Ready to code. What are we building?",
|
|
30
|
+
preferred_model="claude-sonnet-5",
|
|
31
|
+
preferred_tools=["file_read", "file_write", "file_edit", "file_grep", "command_exec", "git_status", "git_diff"],
|
|
32
|
+
temperature=0.3,
|
|
33
|
+
),
|
|
34
|
+
"researcher": DomainPersona(
|
|
35
|
+
name="Researcher",
|
|
36
|
+
slug="researcher",
|
|
37
|
+
icon="??",
|
|
38
|
+
system_prompt=(
|
|
39
|
+
"You are a thorough research assistant. You search the web, read documentation, "
|
|
40
|
+
"cross-reference sources, and synthesize findings into clear, structured answers. "
|
|
41
|
+
"Cite your sources."
|
|
42
|
+
),
|
|
43
|
+
greeting="What would you like me to research?",
|
|
44
|
+
preferred_model="deepseek-chat",
|
|
45
|
+
preferred_tools=["web_search", "http_fetch", "file_read"],
|
|
46
|
+
temperature=0.5,
|
|
47
|
+
),
|
|
48
|
+
"analyst": DomainPersona(
|
|
49
|
+
name="Data Analyst",
|
|
50
|
+
slug="analyst",
|
|
51
|
+
icon="##",
|
|
52
|
+
system_prompt=(
|
|
53
|
+
"You are a data analyst. You examine data, find patterns, create visualizations, "
|
|
54
|
+
"and provide actionable insights. You use code to process and analyze data."
|
|
55
|
+
),
|
|
56
|
+
greeting="Share your data or describe what you'd like to analyze.",
|
|
57
|
+
preferred_model="deepseek-chat",
|
|
58
|
+
preferred_tools=["file_read", "command_exec", "file_write"],
|
|
59
|
+
temperature=0.4,
|
|
60
|
+
),
|
|
61
|
+
"writer": DomainPersona(
|
|
62
|
+
name="Writer",
|
|
63
|
+
slug="writer",
|
|
64
|
+
icon="~~",
|
|
65
|
+
system_prompt=(
|
|
66
|
+
"You are a skilled writer and editor. You help with drafting, editing, "
|
|
67
|
+
"proofreading, and creative writing. You adapt your tone and style to the task."
|
|
68
|
+
),
|
|
69
|
+
greeting="What would you like to write?",
|
|
70
|
+
preferred_model="gpt-5",
|
|
71
|
+
preferred_tools=["web_search", "file_read", "file_write"],
|
|
72
|
+
temperature=0.8,
|
|
73
|
+
),
|
|
74
|
+
"devops": DomainPersona(
|
|
75
|
+
name="DevOps Engineer",
|
|
76
|
+
slug="devops",
|
|
77
|
+
icon="!!",
|
|
78
|
+
system_prompt=(
|
|
79
|
+
"You are a DevOps engineer. You help with deployment, CI/CD, infrastructure, "
|
|
80
|
+
"Docker, Kubernetes, monitoring, and system administration. "
|
|
81
|
+
"You always check system state before making changes."
|
|
82
|
+
),
|
|
83
|
+
greeting="What infrastructure or deployment task do you need help with?",
|
|
84
|
+
preferred_model="claude-sonnet-5",
|
|
85
|
+
preferred_tools=["command_exec", "file_read", "file_write", "file_edit", "git_status"],
|
|
86
|
+
temperature=0.3,
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class PersonaEngine:
|
|
92
|
+
"""Manages domain personas for the agent."""
|
|
93
|
+
|
|
94
|
+
def __init__(self) -> None:
|
|
95
|
+
logger.debug("Entered into PersonaEngine.__init__")
|
|
96
|
+
self._personas: dict[str, DomainPersona] = dict(BUILT_IN_PERSONAS)
|
|
97
|
+
self._active: DomainPersona | None = None
|
|
98
|
+
|
|
99
|
+
def list_personas(self) -> list[DomainPersona]:
|
|
100
|
+
logger.debug("Entered into list_personas")
|
|
101
|
+
return list(self._personas.values())
|
|
102
|
+
|
|
103
|
+
def get(self, slug: str) -> DomainPersona | None:
|
|
104
|
+
logger.debug(f"Entered into get: slug={slug}")
|
|
105
|
+
return self._personas.get(slug)
|
|
106
|
+
|
|
107
|
+
def activate(self, slug: str) -> DomainPersona | None:
|
|
108
|
+
logger.debug(f"Entered into activate: slug={slug}")
|
|
109
|
+
persona = self._personas.get(slug)
|
|
110
|
+
if persona:
|
|
111
|
+
self._active = persona
|
|
112
|
+
return persona
|
|
113
|
+
|
|
114
|
+
def deactivate(self) -> None:
|
|
115
|
+
logger.debug("Entered into deactivate")
|
|
116
|
+
self._active = None
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def active(self) -> DomainPersona | None:
|
|
120
|
+
return self._active
|
|
121
|
+
|
|
122
|
+
def register(self, persona: DomainPersona) -> None:
|
|
123
|
+
logger.debug(f"Entered into register: slug={persona.slug}")
|
|
124
|
+
self._personas[persona.slug] = persona
|
|
125
|
+
|
|
126
|
+
def get_system_prompt_overlay(self) -> str | None:
|
|
127
|
+
logger.debug("Entered into get_system_prompt_overlay")
|
|
128
|
+
if not self._active:
|
|
129
|
+
return None
|
|
130
|
+
return self._active.system_prompt
|