codee-agent 0.7.4__tar.gz → 0.8.0__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.
- {codee_agent-0.7.4 → codee_agent-0.8.0}/PKG-INFO +1 -1
- {codee_agent-0.7.4 → codee_agent-0.8.0}/pyproject.toml +1 -1
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/admin.py +335 -32
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/admin_service.py +29 -8
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/executor.py +60 -10
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/runs_db.py +86 -4
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/test_runs_db.py +47 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/test_trigger_cron_skills.py +86 -13
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/trigger_aws_sqs_skills.py +14 -2
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/trigger_cron_skills.py +15 -5
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/trigger_email_skills.py +15 -2
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/trigger_issue_skills.py +3 -0
- codee_agent-0.8.0/src/codee/test_admin.py +248 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/test_admin_service.py +70 -4
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/test_executor.py +69 -1
- codee_agent-0.8.0/src/codee_agent_abstract/acp.py +201 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_abstract/provider.py +14 -2
- codee_agent-0.8.0/src/codee_agent_abstract/test.py +173 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_claude_code/provider.py +29 -15
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_claude_code/test.py +28 -2
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_codex/provider.py +32 -7
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_codex/test.py +47 -2
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_github_copilot/provider.py +9 -112
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_github_copilot/test.py +31 -53
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_opencode/provider.py +35 -4
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_opencode/test.py +44 -4
- {codee_agent-0.7.4 → codee_agent-0.8.0}/LICENSE +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/README.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/.gitignore +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/admin_api.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/agent_cli.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/coding_agents.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/init_cli.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/claude_key_rotation.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/mcp_config.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/runtime_control.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/test_claude_key_rotation.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/test_mcp_config.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/mail_server.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/setup_wizard.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/start_cli.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/tasks_providers.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/test_agent_cli.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/test_coding_agents.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/test_setup_wizard.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_claude_code/account.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_claude_code/credentials.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_claude_code/oauth.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_claude_code/usage.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_codex/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_agent_opencode/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_database/claude_code_accounts.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_database/database.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_main_context/test_context.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_tasks_abstract/provider.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_tasks_azure_devops/provider.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_tasks_azure_devops/test.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_tasks_jira/provider.py +0 -0
- {codee_agent-0.7.4 → codee_agent-0.8.0}/src/codee_tasks_jira/test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codee-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents.
|
|
5
5
|
Keywords: ai,agent,jira,azure-devops,claude-code,github-copilot,codex,opencode,automation
|
|
6
6
|
Author: Denis Kibalko
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
|
+
import re
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
import reflex as rx
|
|
@@ -21,6 +22,8 @@ RUNS_PAGE_SIZE = 20
|
|
|
21
22
|
# The agents a skill can be run by, as the editor's picker lists them. Built
|
|
22
23
|
# once: which agents exist is decided by this build, not by the settings.
|
|
23
24
|
DEFAULT_AGENT_OPTION = "Default agent"
|
|
25
|
+
# The effort picker's value for "no effort set": a select item can't be empty.
|
|
26
|
+
EFFORT_DEFAULT = "__default__"
|
|
24
27
|
AGENT_NAMES = {agent["code"]: agent["name"] for agent in SERVICE.list_agents()}
|
|
25
28
|
AGENT_CODES = {name: code for code, name in AGENT_NAMES.items()}
|
|
26
29
|
AGENT_OPTIONS = [DEFAULT_AGENT_OPTION, *AGENT_NAMES.values()]
|
|
@@ -48,6 +51,15 @@ def _skill_summary(skill: dict[str, str]) -> SkillSummary:
|
|
|
48
51
|
"agent": AGENT_NAMES.get(skill["agent"], DEFAULT_AGENT_OPTION)})
|
|
49
52
|
|
|
50
53
|
|
|
54
|
+
def _model_option(models: list["ModelOption"],
|
|
55
|
+
model_id: str) -> "ModelOption | None":
|
|
56
|
+
"""The catalog entry for ``model_id``, or None when the catalog lacks it."""
|
|
57
|
+
for model in models:
|
|
58
|
+
if model.id == model_id:
|
|
59
|
+
return model
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
51
63
|
def _usage_row(account: Any) -> "ClaudeAccount":
|
|
52
64
|
"""One account's dashboard row, with its percentages rounded to whole numbers.
|
|
53
65
|
|
|
@@ -91,15 +103,23 @@ class SkillSummary(BaseModel):
|
|
|
91
103
|
# none is run by the default one, which is worth saying on the card.
|
|
92
104
|
agent: str = ""
|
|
93
105
|
model: str = ""
|
|
106
|
+
effort: str = ""
|
|
94
107
|
issue_status: str = ""
|
|
95
108
|
issue_type: str = ""
|
|
96
109
|
|
|
97
110
|
|
|
98
111
|
class ModelOption(BaseModel):
|
|
99
|
-
"""One entry in the skill editor's model picker: code plus friendly name.
|
|
112
|
+
"""One entry in the skill editor's model picker: code plus friendly name.
|
|
113
|
+
|
|
114
|
+
``efforts`` are the reasoning effort levels the model takes, empty when it
|
|
115
|
+
has none, and ``default_effort`` the one it runs at when the skill sets
|
|
116
|
+
none, empty when the agent doesn't say.
|
|
117
|
+
"""
|
|
100
118
|
|
|
101
119
|
id: str
|
|
102
120
|
name: str
|
|
121
|
+
efforts: list[str] = []
|
|
122
|
+
default_effort: str = ""
|
|
103
123
|
|
|
104
124
|
|
|
105
125
|
class ConversationMessage(BaseModel):
|
|
@@ -154,6 +174,7 @@ class RepositorySummary(BaseModel):
|
|
|
154
174
|
|
|
155
175
|
|
|
156
176
|
class ActiveJob(BaseModel):
|
|
177
|
+
id: int = 0
|
|
157
178
|
message: str
|
|
158
179
|
elapsed_label: str
|
|
159
180
|
viewer_url: str
|
|
@@ -186,6 +207,7 @@ def _active_job(job: dict[str, Any]) -> ActiveJob:
|
|
|
186
207
|
url = job.get("task_url") or ""
|
|
187
208
|
linked = bool(url) and message.rstrip().endswith(key)
|
|
188
209
|
return ActiveJob(
|
|
210
|
+
id=job["id"],
|
|
189
211
|
message=message,
|
|
190
212
|
prompt_prefix=message.rstrip()[:-len(key)] if linked else message,
|
|
191
213
|
task_key=key if linked else "",
|
|
@@ -264,6 +286,48 @@ class WorkflowSection(BaseModel):
|
|
|
264
286
|
warnings: list[str] = []
|
|
265
287
|
|
|
266
288
|
|
|
289
|
+
class RunTextPart(BaseModel):
|
|
290
|
+
text: str
|
|
291
|
+
matched: bool = False
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _highlight_parts(text: str, query: str) -> list[RunTextPart]:
|
|
295
|
+
"""Split plain text into safely rendered, case-insensitive search matches."""
|
|
296
|
+
query = query.strip()
|
|
297
|
+
if not text or not query:
|
|
298
|
+
return []
|
|
299
|
+
parts = []
|
|
300
|
+
position = 0
|
|
301
|
+
for match in re.finditer(re.escape(query), text, re.IGNORECASE):
|
|
302
|
+
if match.start() > position:
|
|
303
|
+
parts.append(RunTextPart(text=text[position:match.start()]))
|
|
304
|
+
parts.append(RunTextPart(text=match.group(), matched=True))
|
|
305
|
+
position = match.end()
|
|
306
|
+
if not parts:
|
|
307
|
+
return []
|
|
308
|
+
if position < len(text):
|
|
309
|
+
parts.append(RunTextPart(text=text[position:]))
|
|
310
|
+
return parts
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _run_preview(message: str, user_message: str, response: str,
|
|
314
|
+
query: str) -> str:
|
|
315
|
+
"""Show the first hit in the row, including hits buried in responses."""
|
|
316
|
+
query = query.strip()
|
|
317
|
+
if query:
|
|
318
|
+
for label, text in (("", message), ("Prompt: ", user_message),
|
|
319
|
+
("LLM response: ", response)):
|
|
320
|
+
match = re.search(re.escape(query), text, re.IGNORECASE)
|
|
321
|
+
if match:
|
|
322
|
+
start = max(0, match.start() - 30)
|
|
323
|
+
end = max(start + 120 - len(label), match.end())
|
|
324
|
+
excerpt = text[start:end].replace("\n", " ")
|
|
325
|
+
return (label + ("..." if start else "") + excerpt
|
|
326
|
+
+ ("..." if end < len(text) else ""))
|
|
327
|
+
first_line = message.splitlines()[0] if message else "No message"
|
|
328
|
+
return first_line[:120] + ("..." if len(first_line) > 120 else "")
|
|
329
|
+
|
|
330
|
+
|
|
267
331
|
class RunRecord(BaseModel):
|
|
268
332
|
skill_name: str
|
|
269
333
|
trigger_type: str
|
|
@@ -280,6 +344,10 @@ class RunRecord(BaseModel):
|
|
|
280
344
|
debug_logs: str
|
|
281
345
|
preview: str
|
|
282
346
|
viewer_url: str
|
|
347
|
+
preview_parts: list[RunTextPart] = []
|
|
348
|
+
message_parts: list[RunTextPart] = []
|
|
349
|
+
user_message_parts: list[RunTextPart] = []
|
|
350
|
+
response_parts: list[RunTextPart] = []
|
|
283
351
|
|
|
284
352
|
|
|
285
353
|
class AdminState(rx.State):
|
|
@@ -291,6 +359,8 @@ class AdminState(rx.State):
|
|
|
291
359
|
skill_name: str = ""
|
|
292
360
|
skill_description: str = ""
|
|
293
361
|
skill_model: str = ""
|
|
362
|
+
# The skill's `effort:`, empty for the model's own default.
|
|
363
|
+
skill_effort: str = ""
|
|
294
364
|
# The agent code the skill declares, empty for the default agent.
|
|
295
365
|
skill_agent: str = ""
|
|
296
366
|
skill_type: str = "knowledge"
|
|
@@ -319,6 +389,11 @@ class AdminState(rx.State):
|
|
|
319
389
|
adding_repository: bool = False
|
|
320
390
|
|
|
321
391
|
active_jobs: list[ActiveJob] = []
|
|
392
|
+
# The session the kill confirmation is asking about. Held here rather than
|
|
393
|
+
# read off the row that opened it: the list redraws every second, and a
|
|
394
|
+
# row index can belong to another session by the time "Kill" is pressed.
|
|
395
|
+
kill_job_id: int = 0
|
|
396
|
+
kill_job_label: str = ""
|
|
322
397
|
paused: bool = False
|
|
323
398
|
total_runs: int = 0
|
|
324
399
|
last_24h_runs: int = 0
|
|
@@ -480,6 +555,42 @@ class AdminState(rx.State):
|
|
|
480
555
|
return model.name
|
|
481
556
|
return self.skill_model
|
|
482
557
|
|
|
558
|
+
@rx.var
|
|
559
|
+
def skill_effort_options(self) -> list[str]:
|
|
560
|
+
"""The effort levels the picked model takes, for the effort picker.
|
|
561
|
+
|
|
562
|
+
Empty — and the picker hidden — for a model with no effort control, and
|
|
563
|
+
for one the catalog doesn't know (the agent's default, a hand-typed id,
|
|
564
|
+
or a catalog that hasn't arrived), since nothing says what it takes. An
|
|
565
|
+
effort the skill already carries is always listed, so a value written by
|
|
566
|
+
hand stays visible and can be cleared rather than being dropped unseen.
|
|
567
|
+
"""
|
|
568
|
+
model = _model_option(self.agent_models, self.skill_model)
|
|
569
|
+
options = list(model.efforts) if model else []
|
|
570
|
+
if self.skill_effort and self.skill_effort not in options:
|
|
571
|
+
options.append(self.skill_effort)
|
|
572
|
+
return options
|
|
573
|
+
|
|
574
|
+
@rx.var
|
|
575
|
+
def skill_effort_value(self) -> str:
|
|
576
|
+
"""The effort picker's value; the select can't hold an empty one."""
|
|
577
|
+
return self.skill_effort or EFFORT_DEFAULT
|
|
578
|
+
|
|
579
|
+
@rx.var
|
|
580
|
+
def skill_effort_default_label(self) -> str:
|
|
581
|
+
"""The "no effort set" choice, naming the model's default when known."""
|
|
582
|
+
model = _model_option(self.agent_models, self.skill_model)
|
|
583
|
+
if model and model.default_effort:
|
|
584
|
+
return f"Model default ({model.default_effort})"
|
|
585
|
+
return "Model default"
|
|
586
|
+
|
|
587
|
+
@rx.var
|
|
588
|
+
def skill_effort_unlisted(self) -> bool:
|
|
589
|
+
"""Whether the skill's effort is one the picked model doesn't list."""
|
|
590
|
+
model = _model_option(self.agent_models, self.skill_model)
|
|
591
|
+
return bool(self.skill_effort) and (
|
|
592
|
+
model is None or self.skill_effort not in model.efforts)
|
|
593
|
+
|
|
483
594
|
@rx.var
|
|
484
595
|
def custom_model_query(self) -> str:
|
|
485
596
|
"""The search text when it names no known model, so it can be used as-is."""
|
|
@@ -547,6 +658,7 @@ class AdminState(rx.State):
|
|
|
547
658
|
return None
|
|
548
659
|
self.skill_agent = agent
|
|
549
660
|
self.skill_model = ""
|
|
661
|
+
self.skill_effort = ""
|
|
550
662
|
self.model_query = ""
|
|
551
663
|
return AdminState.load_agent_models
|
|
552
664
|
|
|
@@ -554,9 +666,20 @@ class AdminState(rx.State):
|
|
|
554
666
|
self.model_query = value
|
|
555
667
|
|
|
556
668
|
def choose_model(self, model_id: str) -> None:
|
|
557
|
-
"""Pick a model from the list, or use whatever the user typed.
|
|
669
|
+
"""Pick a model from the list, or use whatever the user typed.
|
|
670
|
+
|
|
671
|
+
An effort the newly picked model is known not to take is cleared, since
|
|
672
|
+
it would only make that model's run fail. One the catalog can't vouch
|
|
673
|
+
either way for stays, and the picker keeps showing it.
|
|
674
|
+
"""
|
|
558
675
|
self.skill_model = model_id.strip()
|
|
559
676
|
self.model_query = ""
|
|
677
|
+
model = _model_option(self.agent_models, self.skill_model)
|
|
678
|
+
if model is not None and self.skill_effort not in model.efforts:
|
|
679
|
+
self.skill_effort = ""
|
|
680
|
+
|
|
681
|
+
def set_skill_effort(self, value: str) -> None:
|
|
682
|
+
self.skill_effort = "" if value == EFFORT_DEFAULT else value
|
|
560
683
|
|
|
561
684
|
@rx.event(background=True)
|
|
562
685
|
async def load_agent_models(self) -> None:
|
|
@@ -633,6 +756,7 @@ class AdminState(rx.State):
|
|
|
633
756
|
self.skill_name = skill["name"]
|
|
634
757
|
self.skill_description = skill["description"]
|
|
635
758
|
self.skill_model = skill["model"]
|
|
759
|
+
self.skill_effort = skill["effort"]
|
|
636
760
|
self.skill_agent = skill["agent"]
|
|
637
761
|
self.model_query = ""
|
|
638
762
|
self.skill_type = skill["type"]
|
|
@@ -657,6 +781,7 @@ class AdminState(rx.State):
|
|
|
657
781
|
"name": self.skill_name,
|
|
658
782
|
"description": self.skill_description,
|
|
659
783
|
"model": self.skill_model,
|
|
784
|
+
"effort": self.skill_effort,
|
|
660
785
|
"agent": self.skill_agent,
|
|
661
786
|
"type": self.skill_type,
|
|
662
787
|
"cron": self.skill_cron,
|
|
@@ -785,8 +910,37 @@ class AdminState(rx.State):
|
|
|
785
910
|
self.last_24h_runs = dashboard["counts"]["last_24h"]
|
|
786
911
|
self.hourly_runs = dashboard["hourly"]
|
|
787
912
|
|
|
788
|
-
def toggle_pause(self) ->
|
|
913
|
+
def toggle_pause(self) -> Any:
|
|
789
914
|
self.paused = SERVICE.set_paused(not self.paused)
|
|
915
|
+
if self.paused:
|
|
916
|
+
message = "New agents won't be spawned."
|
|
917
|
+
if self.active_jobs:
|
|
918
|
+
message += " Currently running agents will continue until completion."
|
|
919
|
+
return rx.toast.info(message)
|
|
920
|
+
|
|
921
|
+
def confirm_kill_job(self, job_id: int, label: str) -> None:
|
|
922
|
+
self.kill_job_id = job_id
|
|
923
|
+
self.kill_job_label = label
|
|
924
|
+
|
|
925
|
+
def set_kill_dialog_open(self, is_open: bool) -> None:
|
|
926
|
+
if not is_open:
|
|
927
|
+
self.kill_job_id = 0
|
|
928
|
+
self.kill_job_label = ""
|
|
929
|
+
|
|
930
|
+
def kill_job(self) -> Any:
|
|
931
|
+
job_id, label = self.kill_job_id, self.kill_job_label
|
|
932
|
+
self.kill_job_id = 0
|
|
933
|
+
self.kill_job_label = ""
|
|
934
|
+
if not job_id:
|
|
935
|
+
return None
|
|
936
|
+
try:
|
|
937
|
+
killed = SERVICE.kill_job(job_id)
|
|
938
|
+
except Exception as error:
|
|
939
|
+
return rx.toast.error(f"Could not kill the session: {error}")
|
|
940
|
+
if not killed:
|
|
941
|
+
return rx.toast.info("That session had already finished.")
|
|
942
|
+
self._refresh_dashboard()
|
|
943
|
+
return rx.toast.success(f"Killed {label}")
|
|
790
944
|
|
|
791
945
|
@rx.event(background=True)
|
|
792
946
|
async def poll_dashboard(self) -> None:
|
|
@@ -860,7 +1014,10 @@ class AdminState(rx.State):
|
|
|
860
1014
|
records = []
|
|
861
1015
|
for run in rows[:limit]:
|
|
862
1016
|
message = (run.get("message") or "").strip()
|
|
863
|
-
|
|
1017
|
+
user_message = (run.get("user_message") or message).strip()
|
|
1018
|
+
response = (run.get("response") or "").strip()
|
|
1019
|
+
preview = _run_preview(message, user_message,
|
|
1020
|
+
response, self.runs_query)
|
|
864
1021
|
records.append(RunRecord(
|
|
865
1022
|
skill_name=run["skill_name"],
|
|
866
1023
|
trigger_type=run["trigger_type"],
|
|
@@ -872,12 +1029,17 @@ class AdminState(rx.State):
|
|
|
872
1029
|
relative_age_label=run["relative_age_label"],
|
|
873
1030
|
session_id=run.get("session_id") or "",
|
|
874
1031
|
message=message,
|
|
875
|
-
user_message=
|
|
876
|
-
response=
|
|
1032
|
+
user_message=user_message,
|
|
1033
|
+
response=response,
|
|
877
1034
|
debug_logs=run.get("debug_logs") or "",
|
|
878
|
-
preview=preview
|
|
1035
|
+
preview=preview,
|
|
879
1036
|
viewer_url=(SERVICE.session_viewer.format(session_id=run["session_id"])
|
|
880
1037
|
if SERVICE.session_viewer and run.get("session_id") else ""),
|
|
1038
|
+
preview_parts=_highlight_parts(preview, self.runs_query),
|
|
1039
|
+
message_parts=_highlight_parts(message, self.runs_query),
|
|
1040
|
+
user_message_parts=_highlight_parts(
|
|
1041
|
+
user_message, self.runs_query),
|
|
1042
|
+
response_parts=_highlight_parts(response, self.runs_query),
|
|
881
1043
|
))
|
|
882
1044
|
return records
|
|
883
1045
|
|
|
@@ -1941,6 +2103,12 @@ def active_job_row(job: ActiveJob) -> rx.Component:
|
|
|
1941
2103
|
aria_label="View session", color=ACCENT, display="flex",
|
|
1942
2104
|
align_items="center"),
|
|
1943
2105
|
),
|
|
2106
|
+
rx.icon_button(
|
|
2107
|
+
rx.icon("square", size=14),
|
|
2108
|
+
size="1", variant="ghost", color_scheme="red",
|
|
2109
|
+
aria_label="Kill session", title="Kill session",
|
|
2110
|
+
on_click=AdminState.confirm_kill_job(job.id, job.message),
|
|
2111
|
+
),
|
|
1944
2112
|
gap="0.85rem",
|
|
1945
2113
|
align="center",
|
|
1946
2114
|
padding="0.85rem 1rem",
|
|
@@ -1952,6 +2120,32 @@ def active_job_row(job: ActiveJob) -> rx.Component:
|
|
|
1952
2120
|
)
|
|
1953
2121
|
|
|
1954
2122
|
|
|
2123
|
+
def kill_job_dialog() -> rx.Component:
|
|
2124
|
+
"""One confirmation for every row, bound to the session it was opened for."""
|
|
2125
|
+
return rx.alert_dialog.root(
|
|
2126
|
+
rx.alert_dialog.content(
|
|
2127
|
+
rx.alert_dialog.title("Kill session"),
|
|
2128
|
+
rx.alert_dialog.description(
|
|
2129
|
+
"This stops ", rx.text.strong(AdminState.kill_job_label),
|
|
2130
|
+
" right away. Whatever the agent was in the middle of is left "
|
|
2131
|
+
"as it is, and the run is logged as killed."),
|
|
2132
|
+
rx.text(
|
|
2133
|
+
"Cron, email and SQS runs are not retried. An issue that is "
|
|
2134
|
+
"still in its trigger status is picked up again on the next "
|
|
2135
|
+
"poll, so pause first if you don't want that.",
|
|
2136
|
+
color=MUTED, font_size="0.85rem", margin_top="0.75rem"),
|
|
2137
|
+
rx.hstack(
|
|
2138
|
+
rx.alert_dialog.cancel(rx.button("Cancel", variant="soft",
|
|
2139
|
+
color_scheme="gray")),
|
|
2140
|
+
rx.alert_dialog.action(rx.button("Kill session", color_scheme="red",
|
|
2141
|
+
on_click=AdminState.kill_job)),
|
|
2142
|
+
spacing="3", justify="end", margin_top="1.25rem", width="100%"),
|
|
2143
|
+
max_width="27rem"),
|
|
2144
|
+
open=AdminState.kill_job_id != 0,
|
|
2145
|
+
on_open_change=AdminState.set_kill_dialog_open,
|
|
2146
|
+
)
|
|
2147
|
+
|
|
2148
|
+
|
|
1955
2149
|
def running_tile() -> rx.Component:
|
|
1956
2150
|
"""Stat tile that lights up while sessions are in flight."""
|
|
1957
2151
|
running = AdminState.active_jobs.length()
|
|
@@ -1984,14 +2178,17 @@ def running_panel() -> rx.Component:
|
|
|
1984
2178
|
animation="codee-breathe 2.4s ease-in-out infinite"),
|
|
1985
2179
|
),
|
|
1986
2180
|
rx.spacer(),
|
|
1987
|
-
rx.
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2181
|
+
rx.cond(
|
|
2182
|
+
is_running | ~AdminState.paused,
|
|
2183
|
+
rx.button(
|
|
2184
|
+
rx.cond(AdminState.paused,
|
|
2185
|
+
rx.icon("play", size=15),
|
|
2186
|
+
rx.icon("pause", size=15)),
|
|
2187
|
+
rx.cond(AdminState.paused, "Unpause", "Pause"),
|
|
2188
|
+
variant="outline",
|
|
2189
|
+
color_scheme=rx.cond(AdminState.paused, "green", "gray"),
|
|
2190
|
+
on_click=AdminState.toggle_pause,
|
|
2191
|
+
),
|
|
1995
2192
|
),
|
|
1996
2193
|
spacing="3",
|
|
1997
2194
|
align="center",
|
|
@@ -2002,10 +2199,22 @@ def running_panel() -> rx.Component:
|
|
|
2002
2199
|
is_running,
|
|
2003
2200
|
rx.vstack(rx.foreach(AdminState.active_jobs, active_job_row),
|
|
2004
2201
|
spacing="2", width="100%"),
|
|
2005
|
-
rx.
|
|
2006
|
-
|
|
2007
|
-
|
|
2202
|
+
rx.cond(
|
|
2203
|
+
AdminState.paused,
|
|
2204
|
+
rx.hstack(
|
|
2205
|
+
rx.icon("pause", size=16, color=SUBTLE_ICON),
|
|
2206
|
+
rx.text("All work is on pause right now.", color=MUTED),
|
|
2207
|
+
rx.button(rx.icon("play", size=15), "Unpause",
|
|
2208
|
+
variant="outline", color_scheme="green",
|
|
2209
|
+
on_click=AdminState.toggle_pause),
|
|
2210
|
+
spacing="3", align="center", flex_wrap="wrap",
|
|
2211
|
+
),
|
|
2212
|
+
rx.hstack(rx.icon("moon", size=16, color=SUBTLE_ICON),
|
|
2213
|
+
rx.text("No sessions running right now.", color=MUTED),
|
|
2214
|
+
spacing="2", align="center"),
|
|
2215
|
+
),
|
|
2008
2216
|
),
|
|
2217
|
+
kill_job_dialog(),
|
|
2009
2218
|
padding="1.25rem",
|
|
2010
2219
|
background=SURFACE,
|
|
2011
2220
|
border=rx.cond(is_running, f"1px solid {ACCENT}", BORDER),
|
|
@@ -2163,6 +2372,9 @@ def skill_card(skill: SkillSummary) -> rx.Component:
|
|
|
2163
2372
|
rx.cond(skill.model != "",
|
|
2164
2373
|
rx.code(skill.model, font_size="0.72rem",
|
|
2165
2374
|
color_scheme="gray")),
|
|
2375
|
+
rx.cond(skill.effort != "",
|
|
2376
|
+
rx.code(skill.effort, font_size="0.72rem",
|
|
2377
|
+
color_scheme="gray")),
|
|
2166
2378
|
spacing="2", align="center", width="100%",
|
|
2167
2379
|
class_name="skill-card-meta"),
|
|
2168
2380
|
rx.cond(
|
|
@@ -2345,6 +2557,39 @@ def model_picker() -> rx.Component:
|
|
|
2345
2557
|
color=MUTED, font_size="0.82rem"))
|
|
2346
2558
|
|
|
2347
2559
|
|
|
2560
|
+
def effort_picker() -> rx.Component:
|
|
2561
|
+
"""Reasoning effort for the picked model, shown only when it takes one."""
|
|
2562
|
+
return rx.cond(
|
|
2563
|
+
AdminState.skill_effort_options.length() > 0,
|
|
2564
|
+
field(
|
|
2565
|
+
"Reasoning effort",
|
|
2566
|
+
rx.select.root(
|
|
2567
|
+
rx.select.trigger(width="100%"),
|
|
2568
|
+
rx.select.content(
|
|
2569
|
+
rx.select.item(AdminState.skill_effort_default_label,
|
|
2570
|
+
value=EFFORT_DEFAULT),
|
|
2571
|
+
rx.foreach(AdminState.skill_effort_options,
|
|
2572
|
+
lambda effort: rx.select.item(effort,
|
|
2573
|
+
value=effort))),
|
|
2574
|
+
value=AdminState.skill_effort_value,
|
|
2575
|
+
on_change=AdminState.set_skill_effort),
|
|
2576
|
+
rx.text(
|
|
2577
|
+
rx.cond(
|
|
2578
|
+
AdminState.skill_effort == "",
|
|
2579
|
+
"Runs at the model's own effort.",
|
|
2580
|
+
rx.cond(
|
|
2581
|
+
AdminState.skill_effort_unlisted,
|
|
2582
|
+
rx.fragment(
|
|
2583
|
+
rx.code(AdminState.skill_effort),
|
|
2584
|
+
" isn't listed for this model, so the agent may "
|
|
2585
|
+
"reject it."),
|
|
2586
|
+
rx.fragment("Saved as ",
|
|
2587
|
+
rx.code("effort: ",
|
|
2588
|
+
AdminState.skill_effort),
|
|
2589
|
+
" in the skill frontmatter."))),
|
|
2590
|
+
color=MUTED, font_size="0.82rem")))
|
|
2591
|
+
|
|
2592
|
+
|
|
2348
2593
|
def delete_skill_dialog() -> rx.Component:
|
|
2349
2594
|
return rx.alert_dialog.root(
|
|
2350
2595
|
rx.alert_dialog.trigger(
|
|
@@ -2384,7 +2629,7 @@ def skill_editor() -> rx.Component:
|
|
|
2384
2629
|
field("Description", rx.text_area(value=AdminState.skill_description,
|
|
2385
2630
|
on_change=AdminState.set_skill_description,
|
|
2386
2631
|
width="100%", min_height="5rem")),
|
|
2387
|
-
rx.grid(agent_picker(), model_picker(),
|
|
2632
|
+
rx.grid(agent_picker(), model_picker(), effort_picker(),
|
|
2388
2633
|
columns=rx.breakpoints(initial="1", md="2"), gap="1rem",
|
|
2389
2634
|
align="start", width="100%"),
|
|
2390
2635
|
rx.cond(AdminState.skill_type == "cron trigger",
|
|
@@ -2441,11 +2686,21 @@ def skills_page() -> rx.Component:
|
|
|
2441
2686
|
rx.button(rx.icon("plus", size=16), "Create",
|
|
2442
2687
|
on_click=AdminState.create_skill),
|
|
2443
2688
|
gap="0.75rem", width="100%"),
|
|
2444
|
-
rx.grid(rx.
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2689
|
+
rx.grid(rx.box(
|
|
2690
|
+
rx.input(placeholder="Search skills", value=AdminState.skill_query,
|
|
2691
|
+
on_change=AdminState.set_skill_query,
|
|
2692
|
+
padding_right="2.5rem", width="100%"),
|
|
2693
|
+
rx.cond(AdminState.skill_query != "",
|
|
2694
|
+
rx.icon_button(rx.icon("x", size=16), variant="ghost", size="1",
|
|
2695
|
+
aria_label="Clear skills search",
|
|
2696
|
+
on_click=AdminState.set_skill_query(
|
|
2697
|
+
""),
|
|
2698
|
+
position="absolute", right="0.5rem", top="50%",
|
|
2699
|
+
transform="translateY(-50%)")),
|
|
2700
|
+
position="relative", width="100%"),
|
|
2701
|
+
rx.select(["All", *SKILL_TYPES], value=AdminState.skill_filter,
|
|
2702
|
+
on_change=AdminState.set_skill_filter, width="100%"),
|
|
2703
|
+
columns=rx.breakpoints(initial="1", md="3fr 1fr"), gap="0.75rem", width="100%"),
|
|
2449
2704
|
rx.cond(AdminState.agents_card_visible | (AdminState.filtered_skills.length() > 0),
|
|
2450
2705
|
rx.grid(rx.cond(AdminState.agents_card_visible, agents_card()),
|
|
2451
2706
|
rx.foreach(AdminState.filtered_skills, skill_card),
|
|
@@ -2582,11 +2837,43 @@ def repositories_page() -> rx.Component:
|
|
|
2582
2837
|
spacing="5", align="start", width="100%"))
|
|
2583
2838
|
|
|
2584
2839
|
|
|
2840
|
+
def highlighted_run_text(text: rx.Var, parts: rx.Var, **props: Any) -> rx.Component:
|
|
2841
|
+
"""Render match segments as text nodes, never as injected HTML."""
|
|
2842
|
+
return rx.cond(
|
|
2843
|
+
parts.length() > 0,
|
|
2844
|
+
rx.text(rx.foreach(parts, lambda part: rx.cond(
|
|
2845
|
+
part.matched,
|
|
2846
|
+
rx.el.mark(part.text, background=rx.color_mode_cond("#ffe082", "#705419"),
|
|
2847
|
+
color=rx.color_mode_cond("#593d00", "#fff4b8"),
|
|
2848
|
+
border_radius="2px"),
|
|
2849
|
+
rx.el.span(part.text))), **props),
|
|
2850
|
+
rx.text(text, **props),
|
|
2851
|
+
)
|
|
2852
|
+
|
|
2853
|
+
|
|
2854
|
+
def run_markdown(text: rx.Var, parts: rx.Var) -> rx.Component:
|
|
2855
|
+
"""A prompt or response rendered as markdown, or as highlighted text while searching.
|
|
2856
|
+
|
|
2857
|
+
Prompts carry issue and email bodies, so raw HTML stays off: a tag in them
|
|
2858
|
+
shows as text instead of reaching the page. Math is off too, so dollar
|
|
2859
|
+
amounts read as written. A search hit falls back to plain text, because
|
|
2860
|
+
the marks can only be drawn over text the page has not parsed.
|
|
2861
|
+
"""
|
|
2862
|
+
return rx.cond(
|
|
2863
|
+
parts.length() > 0,
|
|
2864
|
+
highlighted_run_text(text, parts, white_space="pre-wrap"),
|
|
2865
|
+
rx.box(rx.markdown(text, use_raw=False, use_math=False, use_katex=False),
|
|
2866
|
+
width="100%", overflow_x="auto"),
|
|
2867
|
+
)
|
|
2868
|
+
|
|
2869
|
+
|
|
2585
2870
|
def run_row(run: RunRecord) -> rx.Component:
|
|
2586
2871
|
return rx.box(
|
|
2587
2872
|
rx.flex(
|
|
2588
2873
|
rx.vstack(rx.hstack(rx.text(run.skill_name, font_weight="600"),
|
|
2589
|
-
rx.badge(run.status, color_scheme=rx.cond(
|
|
2874
|
+
rx.badge(run.status, color_scheme=rx.cond(
|
|
2875
|
+
run.status == "succeeded", "green",
|
|
2876
|
+
rx.cond(run.status == "killed", "gray", "red")))),
|
|
2590
2877
|
rx.hstack(
|
|
2591
2878
|
rx.text(local_datetime(run.ended_at)),
|
|
2592
2879
|
rx.text("(", run.relative_age_label, ")"),
|
|
@@ -2599,7 +2886,8 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2599
2886
|
rx.text("Thread ID: ", run.session_id, color=MUTED,
|
|
2600
2887
|
font_size="0.8rem",
|
|
2601
2888
|
font_family="IBM Plex Mono, monospace"),
|
|
2602
|
-
|
|
2889
|
+
highlighted_run_text(
|
|
2890
|
+
run.preview, run.preview_parts, color=MUTED),
|
|
2603
2891
|
rx.cond(run.error != "", rx.text(
|
|
2604
2892
|
run.error, color="#b42318", font_size="0.85rem")),
|
|
2605
2893
|
spacing="2", align="start", flex="1"),
|
|
@@ -2610,11 +2898,16 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2610
2898
|
rx.cond((run.user_message != "") | (run.response != "") | (run.debug_logs != ""), rx.accordion.root(rx.accordion.item(
|
|
2611
2899
|
header="Run info", content=rx.vstack(
|
|
2612
2900
|
rx.text("User message", font_weight="600"),
|
|
2613
|
-
|
|
2901
|
+
run_markdown(run.user_message, run.user_message_parts),
|
|
2902
|
+
rx.cond((run.message != "") & (run.message != run.user_message),
|
|
2903
|
+
rx.fragment(
|
|
2904
|
+
rx.text("Original message", font_weight="600",
|
|
2905
|
+
margin_top="0.75rem"),
|
|
2906
|
+
run_markdown(run.message, run.message_parts))),
|
|
2614
2907
|
rx.cond(run.response != "", rx.fragment(
|
|
2615
2908
|
rx.text("LLM response", font_weight="600",
|
|
2616
2909
|
margin_top="0.75rem"),
|
|
2617
|
-
|
|
2910
|
+
run_markdown(run.response, run.response_parts))),
|
|
2618
2911
|
rx.cond(run.debug_logs != "", rx.fragment(
|
|
2619
2912
|
rx.text("Debug logs", font_weight="600",
|
|
2620
2913
|
margin_top="0.75rem"),
|
|
@@ -2637,10 +2930,20 @@ def runs_page() -> rx.Component:
|
|
|
2637
2930
|
on_click=AdminState.load_more_runs)),
|
|
2638
2931
|
spacing="3", width="100%")
|
|
2639
2932
|
return shell(rx.vstack(page_header("Runs", "Recent trigger executions and outcomes."),
|
|
2640
|
-
rx.
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2933
|
+
rx.box(
|
|
2934
|
+
rx.input(placeholder="Search prompts and LLM responses",
|
|
2935
|
+
value=AdminState.runs_query,
|
|
2936
|
+
on_change=AdminState.search_runs,
|
|
2937
|
+
debounce_timeout=300, padding_right="2.5rem",
|
|
2938
|
+
width="100%"),
|
|
2939
|
+
rx.cond(AdminState.runs_query != "",
|
|
2940
|
+
rx.icon_button(rx.icon("x", size=16), variant="ghost",
|
|
2941
|
+
size="1", aria_label="Clear runs search",
|
|
2942
|
+
on_click=AdminState.search_runs(
|
|
2943
|
+
""),
|
|
2944
|
+
position="absolute", right="0.5rem",
|
|
2945
|
+
top="50%", transform="translateY(-50%)")),
|
|
2946
|
+
position="relative", width="100%"),
|
|
2644
2947
|
rx.cond(AdminState.runs.length() > 0, listing,
|
|
2645
2948
|
rx.cond(AdminState.runs_query != "",
|
|
2646
2949
|
empty_state(
|