codee-agent 0.7.5__tar.gz → 0.8.1__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.5 → codee_agent-0.8.1}/PKG-INFO +1 -1
- {codee_agent-0.7.5 → codee_agent-0.8.1}/pyproject.toml +1 -1
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/admin.py +336 -31
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/admin_service.py +29 -8
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/executor.py +60 -10
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/runs_db.py +86 -4
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/test_runs_db.py +47 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/test_trigger_cron_skills.py +86 -13
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/trigger_aws_sqs_skills.py +14 -2
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/trigger_cron_skills.py +15 -5
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/trigger_email_skills.py +15 -2
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/trigger_issue_skills.py +3 -0
- codee_agent-0.8.1/src/codee/test_admin.py +248 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/test_admin_service.py +70 -4
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/test_executor.py +69 -1
- codee_agent-0.8.1/src/codee_agent_abstract/acp.py +201 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_abstract/provider.py +14 -2
- codee_agent-0.8.1/src/codee_agent_abstract/test.py +173 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_claude_code/provider.py +29 -15
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_claude_code/test.py +28 -2
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_codex/provider.py +32 -7
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_codex/test.py +47 -2
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_github_copilot/provider.py +9 -112
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_github_copilot/test.py +31 -53
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_opencode/provider.py +35 -4
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_opencode/test.py +44 -4
- codee_agent-0.7.5/src/codee/test_admin.py +0 -41
- {codee_agent-0.7.5 → codee_agent-0.8.1}/LICENSE +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/README.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/.gitignore +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/admin_api.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/agent_cli.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/coding_agents.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/init_cli.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/claude_key_rotation.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/mcp_config.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/runtime_control.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/test_claude_key_rotation.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/test_mcp_config.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/mail_server.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/setup_wizard.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/start_cli.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/tasks_providers.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/test_agent_cli.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/test_coding_agents.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/test_setup_wizard.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_claude_code/account.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_claude_code/credentials.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_claude_code/oauth.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_claude_code/usage.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_codex/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_agent_opencode/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_database/claude_code_accounts.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_database/database.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_main_context/test_context.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_tasks_abstract/provider.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_tasks_azure_devops/provider.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_tasks_azure_devops/test.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/src/codee_tasks_jira/provider.py +0 -0
- {codee_agent-0.7.5 → codee_agent-0.8.1}/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.1
|
|
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,
|
|
@@ -793,6 +918,30 @@ class AdminState(rx.State):
|
|
|
793
918
|
message += " Currently running agents will continue until completion."
|
|
794
919
|
return rx.toast.info(message)
|
|
795
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}")
|
|
944
|
+
|
|
796
945
|
@rx.event(background=True)
|
|
797
946
|
async def poll_dashboard(self) -> None:
|
|
798
947
|
"""Keep the dashboard live for as long as it is the page on screen.
|
|
@@ -865,7 +1014,10 @@ class AdminState(rx.State):
|
|
|
865
1014
|
records = []
|
|
866
1015
|
for run in rows[:limit]:
|
|
867
1016
|
message = (run.get("message") or "").strip()
|
|
868
|
-
|
|
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)
|
|
869
1021
|
records.append(RunRecord(
|
|
870
1022
|
skill_name=run["skill_name"],
|
|
871
1023
|
trigger_type=run["trigger_type"],
|
|
@@ -877,12 +1029,17 @@ class AdminState(rx.State):
|
|
|
877
1029
|
relative_age_label=run["relative_age_label"],
|
|
878
1030
|
session_id=run.get("session_id") or "",
|
|
879
1031
|
message=message,
|
|
880
|
-
user_message=
|
|
881
|
-
response=
|
|
1032
|
+
user_message=user_message,
|
|
1033
|
+
response=response,
|
|
882
1034
|
debug_logs=run.get("debug_logs") or "",
|
|
883
|
-
preview=preview
|
|
1035
|
+
preview=preview,
|
|
884
1036
|
viewer_url=(SERVICE.session_viewer.format(session_id=run["session_id"])
|
|
885
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),
|
|
886
1043
|
))
|
|
887
1044
|
return records
|
|
888
1045
|
|
|
@@ -1946,6 +2103,12 @@ def active_job_row(job: ActiveJob) -> rx.Component:
|
|
|
1946
2103
|
aria_label="View session", color=ACCENT, display="flex",
|
|
1947
2104
|
align_items="center"),
|
|
1948
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
|
+
),
|
|
1949
2112
|
gap="0.85rem",
|
|
1950
2113
|
align="center",
|
|
1951
2114
|
padding="0.85rem 1rem",
|
|
@@ -1957,6 +2120,32 @@ def active_job_row(job: ActiveJob) -> rx.Component:
|
|
|
1957
2120
|
)
|
|
1958
2121
|
|
|
1959
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
|
+
|
|
1960
2149
|
def running_tile() -> rx.Component:
|
|
1961
2150
|
"""Stat tile that lights up while sessions are in flight."""
|
|
1962
2151
|
running = AdminState.active_jobs.length()
|
|
@@ -1989,14 +2178,17 @@ def running_panel() -> rx.Component:
|
|
|
1989
2178
|
animation="codee-breathe 2.4s ease-in-out infinite"),
|
|
1990
2179
|
),
|
|
1991
2180
|
rx.spacer(),
|
|
1992
|
-
rx.
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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
|
+
),
|
|
2000
2192
|
),
|
|
2001
2193
|
spacing="3",
|
|
2002
2194
|
align="center",
|
|
@@ -2007,10 +2199,22 @@ def running_panel() -> rx.Component:
|
|
|
2007
2199
|
is_running,
|
|
2008
2200
|
rx.vstack(rx.foreach(AdminState.active_jobs, active_job_row),
|
|
2009
2201
|
spacing="2", width="100%"),
|
|
2010
|
-
rx.
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
+
),
|
|
2013
2216
|
),
|
|
2217
|
+
kill_job_dialog(),
|
|
2014
2218
|
padding="1.25rem",
|
|
2015
2219
|
background=SURFACE,
|
|
2016
2220
|
border=rx.cond(is_running, f"1px solid {ACCENT}", BORDER),
|
|
@@ -2168,6 +2372,9 @@ def skill_card(skill: SkillSummary) -> rx.Component:
|
|
|
2168
2372
|
rx.cond(skill.model != "",
|
|
2169
2373
|
rx.code(skill.model, font_size="0.72rem",
|
|
2170
2374
|
color_scheme="gray")),
|
|
2375
|
+
rx.cond(skill.effort != "",
|
|
2376
|
+
rx.code(skill.effort, font_size="0.72rem",
|
|
2377
|
+
color_scheme="gray")),
|
|
2171
2378
|
spacing="2", align="center", width="100%",
|
|
2172
2379
|
class_name="skill-card-meta"),
|
|
2173
2380
|
rx.cond(
|
|
@@ -2350,6 +2557,40 @@ def model_picker() -> rx.Component:
|
|
|
2350
2557
|
color=MUTED, font_size="0.82rem"))
|
|
2351
2558
|
|
|
2352
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
|
+
rx.box(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
|
+
flex="1", min_width="0"))
|
|
2592
|
+
|
|
2593
|
+
|
|
2353
2594
|
def delete_skill_dialog() -> rx.Component:
|
|
2354
2595
|
return rx.alert_dialog.root(
|
|
2355
2596
|
rx.alert_dialog.trigger(
|
|
@@ -2389,7 +2630,13 @@ def skill_editor() -> rx.Component:
|
|
|
2389
2630
|
field("Description", rx.text_area(value=AdminState.skill_description,
|
|
2390
2631
|
on_change=AdminState.set_skill_description,
|
|
2391
2632
|
width="100%", min_height="5rem")),
|
|
2392
|
-
rx.grid(agent_picker(),
|
|
2633
|
+
rx.grid(agent_picker(),
|
|
2634
|
+
# Effort shares the model's column; the model takes all of it
|
|
2635
|
+
# when the picked model has no effort levels.
|
|
2636
|
+
rx.flex(rx.box(model_picker(), flex="1", min_width="0"),
|
|
2637
|
+
effort_picker(),
|
|
2638
|
+
direction=rx.breakpoints(initial="column", sm="row"),
|
|
2639
|
+
gap="1rem", align="start", width="100%"),
|
|
2393
2640
|
columns=rx.breakpoints(initial="1", md="2"), gap="1rem",
|
|
2394
2641
|
align="start", width="100%"),
|
|
2395
2642
|
rx.cond(AdminState.skill_type == "cron trigger",
|
|
@@ -2446,11 +2693,21 @@ def skills_page() -> rx.Component:
|
|
|
2446
2693
|
rx.button(rx.icon("plus", size=16), "Create",
|
|
2447
2694
|
on_click=AdminState.create_skill),
|
|
2448
2695
|
gap="0.75rem", width="100%"),
|
|
2449
|
-
rx.grid(rx.
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2696
|
+
rx.grid(rx.box(
|
|
2697
|
+
rx.input(placeholder="Search skills", value=AdminState.skill_query,
|
|
2698
|
+
on_change=AdminState.set_skill_query,
|
|
2699
|
+
padding_right="2.5rem", width="100%"),
|
|
2700
|
+
rx.cond(AdminState.skill_query != "",
|
|
2701
|
+
rx.icon_button(rx.icon("x", size=16), variant="ghost", size="1",
|
|
2702
|
+
aria_label="Clear skills search",
|
|
2703
|
+
on_click=AdminState.set_skill_query(
|
|
2704
|
+
""),
|
|
2705
|
+
position="absolute", right="0.5rem", top="50%",
|
|
2706
|
+
transform="translateY(-50%)")),
|
|
2707
|
+
position="relative", width="100%"),
|
|
2708
|
+
rx.select(["All", *SKILL_TYPES], value=AdminState.skill_filter,
|
|
2709
|
+
on_change=AdminState.set_skill_filter, width="100%"),
|
|
2710
|
+
columns=rx.breakpoints(initial="1", md="3fr 1fr"), gap="0.75rem", width="100%"),
|
|
2454
2711
|
rx.cond(AdminState.agents_card_visible | (AdminState.filtered_skills.length() > 0),
|
|
2455
2712
|
rx.grid(rx.cond(AdminState.agents_card_visible, agents_card()),
|
|
2456
2713
|
rx.foreach(AdminState.filtered_skills, skill_card),
|
|
@@ -2587,11 +2844,43 @@ def repositories_page() -> rx.Component:
|
|
|
2587
2844
|
spacing="5", align="start", width="100%"))
|
|
2588
2845
|
|
|
2589
2846
|
|
|
2847
|
+
def highlighted_run_text(text: rx.Var, parts: rx.Var, **props: Any) -> rx.Component:
|
|
2848
|
+
"""Render match segments as text nodes, never as injected HTML."""
|
|
2849
|
+
return rx.cond(
|
|
2850
|
+
parts.length() > 0,
|
|
2851
|
+
rx.text(rx.foreach(parts, lambda part: rx.cond(
|
|
2852
|
+
part.matched,
|
|
2853
|
+
rx.el.mark(part.text, background=rx.color_mode_cond("#ffe082", "#705419"),
|
|
2854
|
+
color=rx.color_mode_cond("#593d00", "#fff4b8"),
|
|
2855
|
+
border_radius="2px"),
|
|
2856
|
+
rx.el.span(part.text))), **props),
|
|
2857
|
+
rx.text(text, **props),
|
|
2858
|
+
)
|
|
2859
|
+
|
|
2860
|
+
|
|
2861
|
+
def run_markdown(text: rx.Var, parts: rx.Var) -> rx.Component:
|
|
2862
|
+
"""A prompt or response rendered as markdown, or as highlighted text while searching.
|
|
2863
|
+
|
|
2864
|
+
Prompts carry issue and email bodies, so raw HTML stays off: a tag in them
|
|
2865
|
+
shows as text instead of reaching the page. Math is off too, so dollar
|
|
2866
|
+
amounts read as written. A search hit falls back to plain text, because
|
|
2867
|
+
the marks can only be drawn over text the page has not parsed.
|
|
2868
|
+
"""
|
|
2869
|
+
return rx.cond(
|
|
2870
|
+
parts.length() > 0,
|
|
2871
|
+
highlighted_run_text(text, parts, white_space="pre-wrap"),
|
|
2872
|
+
rx.box(rx.markdown(text, use_raw=False, use_math=False, use_katex=False),
|
|
2873
|
+
width="100%", overflow_x="auto"),
|
|
2874
|
+
)
|
|
2875
|
+
|
|
2876
|
+
|
|
2590
2877
|
def run_row(run: RunRecord) -> rx.Component:
|
|
2591
2878
|
return rx.box(
|
|
2592
2879
|
rx.flex(
|
|
2593
2880
|
rx.vstack(rx.hstack(rx.text(run.skill_name, font_weight="600"),
|
|
2594
|
-
rx.badge(run.status, color_scheme=rx.cond(
|
|
2881
|
+
rx.badge(run.status, color_scheme=rx.cond(
|
|
2882
|
+
run.status == "succeeded", "green",
|
|
2883
|
+
rx.cond(run.status == "killed", "gray", "red")))),
|
|
2595
2884
|
rx.hstack(
|
|
2596
2885
|
rx.text(local_datetime(run.ended_at)),
|
|
2597
2886
|
rx.text("(", run.relative_age_label, ")"),
|
|
@@ -2604,7 +2893,8 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2604
2893
|
rx.text("Thread ID: ", run.session_id, color=MUTED,
|
|
2605
2894
|
font_size="0.8rem",
|
|
2606
2895
|
font_family="IBM Plex Mono, monospace"),
|
|
2607
|
-
|
|
2896
|
+
highlighted_run_text(
|
|
2897
|
+
run.preview, run.preview_parts, color=MUTED),
|
|
2608
2898
|
rx.cond(run.error != "", rx.text(
|
|
2609
2899
|
run.error, color="#b42318", font_size="0.85rem")),
|
|
2610
2900
|
spacing="2", align="start", flex="1"),
|
|
@@ -2615,11 +2905,16 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2615
2905
|
rx.cond((run.user_message != "") | (run.response != "") | (run.debug_logs != ""), rx.accordion.root(rx.accordion.item(
|
|
2616
2906
|
header="Run info", content=rx.vstack(
|
|
2617
2907
|
rx.text("User message", font_weight="600"),
|
|
2618
|
-
|
|
2908
|
+
run_markdown(run.user_message, run.user_message_parts),
|
|
2909
|
+
rx.cond((run.message != "") & (run.message != run.user_message),
|
|
2910
|
+
rx.fragment(
|
|
2911
|
+
rx.text("Original message", font_weight="600",
|
|
2912
|
+
margin_top="0.75rem"),
|
|
2913
|
+
run_markdown(run.message, run.message_parts))),
|
|
2619
2914
|
rx.cond(run.response != "", rx.fragment(
|
|
2620
2915
|
rx.text("LLM response", font_weight="600",
|
|
2621
2916
|
margin_top="0.75rem"),
|
|
2622
|
-
|
|
2917
|
+
run_markdown(run.response, run.response_parts))),
|
|
2623
2918
|
rx.cond(run.debug_logs != "", rx.fragment(
|
|
2624
2919
|
rx.text("Debug logs", font_weight="600",
|
|
2625
2920
|
margin_top="0.75rem"),
|
|
@@ -2642,10 +2937,20 @@ def runs_page() -> rx.Component:
|
|
|
2642
2937
|
on_click=AdminState.load_more_runs)),
|
|
2643
2938
|
spacing="3", width="100%")
|
|
2644
2939
|
return shell(rx.vstack(page_header("Runs", "Recent trigger executions and outcomes."),
|
|
2645
|
-
rx.
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2940
|
+
rx.box(
|
|
2941
|
+
rx.input(placeholder="Search prompts and LLM responses",
|
|
2942
|
+
value=AdminState.runs_query,
|
|
2943
|
+
on_change=AdminState.search_runs,
|
|
2944
|
+
debounce_timeout=300, padding_right="2.5rem",
|
|
2945
|
+
width="100%"),
|
|
2946
|
+
rx.cond(AdminState.runs_query != "",
|
|
2947
|
+
rx.icon_button(rx.icon("x", size=16), variant="ghost",
|
|
2948
|
+
size="1", aria_label="Clear runs search",
|
|
2949
|
+
on_click=AdminState.search_runs(
|
|
2950
|
+
""),
|
|
2951
|
+
position="absolute", right="0.5rem",
|
|
2952
|
+
top="50%", transform="translateY(-50%)")),
|
|
2953
|
+
position="relative", width="100%"),
|
|
2649
2954
|
rx.cond(AdminState.runs.length() > 0, listing,
|
|
2650
2955
|
rx.cond(AdminState.runs_query != "",
|
|
2651
2956
|
empty_state(
|