Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ ANTHROPIC_API_KEY=
OPEN_ROUTER_API_KEY=
XAI_API_KEY=

# Optional provider-compatible endpoints. OpenAI-compatible URLs include `/v1`;
# Anthropic URLs are the API origin because the SDK appends `/v1`. Gemini and
# Vertex values override the SDK service endpoint. Per-model `api_base` in
# artemis.jsonc overrides these values.
# GOOGLE_BASE_URL=https://generativelanguage.googleapis.com
# VERTEX_AI_BASE_URL=https://us-central1-aiplatform.googleapis.com
# OPENAI_BASE_URL=https://openai-proxy.example/v1
# ANTHROPIC_BASE_URL=https://anthropic-proxy.example
# OPEN_ROUTER_BASE_URL=https://openrouter.ai/api/v1
# XAI_BASE_URL=https://api.x.ai/v1
# OLLAMA_BASE_URL=http://localhost:11434/v1
# VLLM_BASE_URL=http://localhost:8000/v1
# CUSTOM_BASE_URL=http://localhost:8000/v1

# Google Cloud Vision OCR (Optional - for advanced OCR processing)
OCR_API_KEY=

Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
UV_NO_PROGRESS: "1"

jobs:
# Windows leg removed org-wide per CHE-522 (Cheese: Linux CI only, no
# Windows jobs or artifacts).
python-quality:
name: Python 3.12 / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
Expand All @@ -25,15 +27,14 @@
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
- blacksmith-2vcpu-ubuntu-2404

steps:
- name: Check out repository
uses: actions/checkout@v4

Check failure on line 34 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 34 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

ci.yml:34: unpinned action reference: action is not pinned to a hash (required by blanket policy)

- name: Set up Python 3.12
uses: actions/setup-python@v5

Check failure on line 37 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 37 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

ci.yml:37: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
python-version: "3.12"

Expand All @@ -41,7 +42,7 @@
run: python scripts/check_dependency_sources.py

- name: Set up uv
uses: astral-sh/setup-uv@v6

Check failure on line 45 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 45 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

ci.yml:45: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
enable-cache: true
cache-dependency-glob: uv.lock
Expand Down Expand Up @@ -78,7 +79,7 @@

frontend:
name: Frontend tests and production build
runs-on: ubuntu-latest
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 20
defaults:
run:
Expand All @@ -86,10 +87,10 @@

steps:
- name: Check out repository
uses: actions/checkout@v4

Check failure on line 90 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 90 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

ci.yml:90: unpinned action reference: action is not pinned to a hash (required by blanket policy)

- name: Set up Node.js
uses: actions/setup-node@v4

Check failure on line 93 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 93 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

ci.yml:93: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
node-version: "22.22.3"
cache: npm
Expand All @@ -106,26 +107,26 @@

package:
name: Build and smoke-test Python packages
runs-on: ubuntu-latest
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 30

steps:
- name: Check out repository
uses: actions/checkout@v4

Check failure on line 115 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 115 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

ci.yml:115: unpinned action reference: action is not pinned to a hash (required by blanket policy)

- name: Set up Python 3.12
uses: actions/setup-python@v5

Check failure on line 118 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 118 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

ci.yml:118: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
python-version: "3.12"

- name: Set up uv
uses: astral-sh/setup-uv@v6

Check failure on line 123 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 123 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

ci.yml:123: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
enable-cache: true
cache-dependency-glob: uv.lock

- name: Set up Node.js
uses: actions/setup-node@v4

Check failure on line 129 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/unpinned-uses

unpinned action reference: action is not pinned to a hash (required by blanket policy)

Check failure on line 129 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

ci.yml:129: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
node-version: "22.22.3"
cache: npm
Expand Down Expand Up @@ -171,7 +172,7 @@
artemis --help

- name: Upload distributions
uses: actions/upload-artifact@v4

Check failure on line 175 in .github/workflows/ci.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

unpinned-uses

ci.yml:175: unpinned action reference: action is not pinned to a hash (required by blanket policy)
with:
name: python-distributions
path: dist/*
Expand Down
2 changes: 1 addition & 1 deletion .quality-baseline.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"broad_exception_handlers": 754,
"broad_exception_handlers": 759,
"silent_broad_exception_handlers": 0,
"type_ignore_comments": 18
}
34 changes: 34 additions & 0 deletions apps/admin_console/database/repositories/session_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,40 @@ def get_session_status(self, session_id: str) -> str | None:
except Exception:
return None

def create_queued_session(
self,
session_id: str,
goal: str,
profile: str,
device_serial: str | None,
start_time: float | None = None,
) -> bool:
"""Persist a queue item before its worker starts a session."""
try:
with db_session(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO sessions "
"(session_id, initial_goal, start_time, end_time, status, device_info, pid) "
"VALUES (?, ?, ?, NULL, 'queued', ?, NULL)",
(
str(session_id),
goal,
start_time if start_time is not None else time.time(),
json.dumps(
{
"profile": profile,
"device_id": device_serial,
}
),
),
)
conn.commit()
return cursor.rowcount > 0
except Exception:
logger.exception("Could not create queued session %s", session_id)
return False

def update_session_status(
self, session_id: str, status: str, end_time: float | None = None
) -> bool:
Expand Down
24 changes: 17 additions & 7 deletions apps/admin_console/routers/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,12 @@ def mask_key(k: str | None) -> str | None:
anthropic_real = get_real_env_value("ANTHROPIC_API_KEY", "anthropic")
openrouter_real = get_real_env_value("OPEN_ROUTER_API_KEY", "openrouter")
xai_real = get_real_env_value("XAI_API_KEY", "xai")
base_url_val = settings.OPENAI_BASE_URL or os.environ.get("OPENAI_BASE_URL")
if base_url_val and is_placeholder_key(base_url_val):
base_url_val = None
openai_base_url = settings.OPENAI_BASE_URL or os.environ.get("OPENAI_BASE_URL")
anthropic_base_url = settings.ANTHROPIC_BASE_URL or os.environ.get("ANTHROPIC_BASE_URL")
if openai_base_url and is_placeholder_key(openai_base_url):
openai_base_url = None
if anthropic_base_url and is_placeholder_key(anthropic_base_url):
anthropic_base_url = None
ocr_real = get_real_env_value("OCR_API_KEY", "ocr") or get_real_env_value(
"VISION_API_KEY", "ocr"
)
Expand Down Expand Up @@ -467,10 +470,17 @@ def mask_key(k: str | None) -> str | None:
},
{
"name": "OPENAI_BASE_URL",
"provider": "custom",
"is_set": bool(base_url_val),
"preview": base_url_val,
"description": "Custom API endpoint (for local Ollama, vLLM, DeepSeek, or proxies)",
"provider": "openai",
"is_set": bool(openai_base_url),
"preview": openai_base_url,
"description": "Custom OpenAI-compatible API endpoint",
},
{
"name": "ANTHROPIC_BASE_URL",
"provider": "anthropic",
"is_set": bool(anthropic_base_url),
"preview": anthropic_base_url,
"description": "Custom Anthropic-compatible API endpoint",
},
{
"name": "VISION_API_KEY",
Expand Down
1 change: 1 addition & 0 deletions apps/admin_console/routers/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ async def run_task(request: RunRequest):
ingress=request.ingress or "frontend",
session_id=request.session_id,
conversation_id=request.conversation_id,
run_id=request.run_id,
)


Expand Down
1 change: 1 addition & 0 deletions apps/admin_console/schemas/task_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class RunRequest(BaseModel):
ingress: str | None = "frontend"
session_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None


class ReplayRequest(BaseModel):
Expand Down
69 changes: 69 additions & 0 deletions apps/admin_console/services/task_queue_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ def _build_worker_invocation(
explorer_mode = task_item.get("explorer_mode")
locked_app = task_item.get("locked_app_package") or task_item.get("locked_app")
app_path = task_item.get("app_path")
run_id = task_item.get("run_id")

test_name = f"web_{int(time.time())}_{run_key[:8]}"
env = os.environ.copy()
Expand Down Expand Up @@ -543,6 +544,8 @@ def _build_worker_invocation(
]
if sess_id:
cmd.extend(["--session-id", str(sess_id)])
if run_id:
cmd.extend(["--run-id", str(run_id)])
if expected_output:
cmd.extend(["--output-description", str(expected_output)])
if enable_outputter is not None:
Expand Down Expand Up @@ -1004,6 +1007,7 @@ def _create_queue_item(
conversation_id: str | None,
verification_level: str | None = None,
explorer_mode: str | None = None,
run_id: str | None = None,
) -> dict[str, Any]:
"""Reserve a device slot and build one pending queue item for a goal."""
sess_id = single_session_id if single_session_id else str(uuid.uuid4())
Expand Down Expand Up @@ -1031,6 +1035,7 @@ def _create_queue_item(
"adb_endpoint": endpoint.to_dict(),
"ingress": ingress,
"conversation_id": conversation_id,
"run_id": run_id,
"status": "pending",
"queue_ticket": queue_ticket,
"created_at": now + index * 0.001,
Expand All @@ -1052,12 +1057,18 @@ async def enqueue_tasks(
conversation_id: str | None = None,
verification_level: str | None = None,
explorer_mode: str | None = None,
run_id: str | None = None,
) -> dict[str, Any]:
"""Enqueues one or more goals and wakes up the background worker.

``verification_level`` and ``explorer_mode`` are Pro-profile tuning knobs
forwarded to the worker as ``--verification-level`` / ``--explorer-pro-mode``;
they are normalised here so the queue item and the CLI see one spelling.

``run_id`` is the Gate 1 batch-grouping key (see
``artemis.config.attempt_lifecycle_hooks``); it is forwarded to the
spawned worker as ``--run-id`` so daemon-dispatched attempts get the
same manifest/reconciliation evidence as standalone runs.
"""
verification_level = (
str(verification_level).strip().lower() or None if verification_level else None
Expand All @@ -1066,6 +1077,7 @@ async def enqueue_tasks(
cls.ensure_worker_running()

enqueued_tasks = []
created_trace_session_ids: set[str] = set()
now = time.time()
endpoint = current_adb_endpoint()

Expand Down Expand Up @@ -1105,7 +1117,64 @@ async def enqueue_tasks(
conversation_id,
verification_level=verification_level,
explorer_mode=explorer_mode,
run_id=run_id,
)
session_id = str(task_item["session_id"])
existing_trace = trace_store.read_status(session_id)
trace_created = existing_trace is None
existing_trace_is_terminal = bool(
existing_trace
and existing_trace.get("status") in {"completed", "failed", "cancelled", "success"}
)
try:
if existing_trace_is_terminal:
raise RuntimeError(f"Session {session_id} already has a terminal trace")
if trace_created:
trace_store.init_trace(
session_id,
goal,
task_item["profile"],
task_item.get("conversation_id"),
task_item.get("device_serial"),
)
created_trace_session_ids.add(session_id)
if not session_repo.create_queued_session(
session_id,
goal,
task_item["profile"],
task_item.get("device_serial"),
task_item.get("start_time"),
):
raise RuntimeError(f"Could not persist queued session {session_id}")
except (OSError, RuntimeError) as exc:
DeviceExecutionLock.cancel_reservation(task_item.get("queue_ticket"))
if trace_created or (
not existing_trace_is_terminal
and session_repo.get_session_by_id(session_id) is None
):
try:
trace_store.update_trace_status(session_id, "failed", error=str(exc))
except OSError:
logger.exception(
"Could not mark queue setup failure for session %s", session_id
)
for enqueued_task in enqueued_tasks:
enqueued_session_id = str(enqueued_task["session_id"])
session_repo.update_session_status(enqueued_session_id, "failed", time.time())
if enqueued_session_id in created_trace_session_ids:
try:
trace_store.update_trace_status(
enqueued_session_id,
"failed",
error="Task batch could not be queued.",
)
except OSError:
logger.exception(
"Could not mark rolled-back queue session %s failed",
enqueued_session_id,
)
cls._remove_task(enqueued_session_id)
raise
state.queue_items.append(task_item)
enqueued_tasks.append(task_item)
cls._broadcast_startup_progress(
Expand Down
9 changes: 1 addition & 8 deletions artemis/agents/flash/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@
from artemis.mcp.observation import observe
from artemis.memory.transcript import PRO_UI_LIST_MARKER, TranscriptLedger, mark_ephemeral
from artemis.services.llm import (
RobustChatModelWrapper,
acomplete,
get_google_llm,
get_llm,
invoke_llm_with_timeout_message,
)
Expand Down Expand Up @@ -279,12 +277,7 @@ def _build_ledger(self) -> TranscriptLedger:

def _init_llm(self):
"""Initializes the Universal LLM via the Service Layer."""
try:
return get_llm(self.ctx, name="operator")
except Exception as e:
logger.warning(f"Failed to get operator LLM from config, using default: {e}")

return RobustChatModelWrapper(get_google_llm(model_name="gemini-2.5-flash"), self.ctx)
return get_llm(self.ctx, name="operator")

def _render_system_prompt(self, tools_declaration: list) -> str:
"""Renders the system prompt from the flash_runner.md template.
Expand Down
28 changes: 16 additions & 12 deletions artemis/agents/flash/summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

from artemis.context import ArtemisContext
from artemis.memory.step_memory import JobKey, StepMemoryService
from artemis.services.llm import RobustChatModelWrapper, get_google_llm, get_llm
from artemis.services.llm import RobustChatModelWrapper, get_llm
from artemis.services.token_meter import record_llm_usage
from artemis.utils.logger import get_logger
from artemis.utils.task_tree import format_actions_clean
Expand Down Expand Up @@ -161,18 +161,22 @@ def __init__(
flush_timeout_s=flush_timeout_s,
)

# Initialize lightweight VLM: prioritize explicit model_name
target_model = model_name or "gemini-2.5-flash-lite"
self._model_name = target_model
try:
if model_name:
self._llm = get_google_llm(model_name=target_model, temperature=0.0)
else:
self._llm = get_llm(ctx, name="summarizer", is_utils=True)
except Exception:
self._llm = get_google_llm(model_name=target_model, temperature=0.0)
# The summarizer role carries the provider and default model. This profile
# knob only overrides that model when the caller explicitly supplies one.
self._model_name = model_name or ""
self._llm = get_llm(
ctx,
name="summarizer",
temperature=0.0,
model_name=model_name or None,
)
try:
configured = getattr(self._llm, "model", None) or getattr(self._llm, "model_name", None)
endpoint = getattr(self._llm, "endpoint", None)
configured = getattr(endpoint, "model_name", None)
if not configured:
configured = getattr(self._llm, "model", None) or getattr(
self._llm, "model_name", None
)
if isinstance(configured, str) and configured:
self._model_name = configured
except Exception as exc:
Expand Down
13 changes: 9 additions & 4 deletions artemis/agents/image_processor/image_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,15 @@ def __init__(self, ctx: ArtemisContext):

@trace(type="agent", name="image_processor")
async def run(self, instruction: str, target_image_path: str) -> dict:
try:
llm = get_llm(self.ctx, name="image_processor")
except Exception:
llm = get_llm(self.ctx, name="operator")
# There is no ``image_processor`` node on ``LLMConfig`` or
# ``LLMConfigUtils``, so the former ``get_llm(name="image_processor")``
# attempt raised ``AttributeError`` on every call and a bare handler
# silently resolved the operator model instead. The operator model is
# what this agent has always actually used; say so rather than hiding
# it behind a lookup that cannot succeed. Giving the image processor
# its own configurable node is a config-schema change, tracked
# separately.
llm = get_llm(self.ctx, name="operator")

base_dir = settings.TRACES_PATH
image_processor_dir = base_dir / "images" / "image_processor"
Expand Down
9 changes: 7 additions & 2 deletions artemis/agents/object_detector/object_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,13 @@ async def _run_object_detection(
queries = queries or []
templates = templates or ["Point to the following objects: {labels_str}"]
try:
llm = get_llm(ctx, name="object_detector")
except Exception:
llm = get_llm(ctx, name="object_detector", is_utils=True)
except (ValueError, AttributeError) as exc:
logger.warning(
f"object_detector is not configured ({exc}); falling back to the operator "
"model, which is slower and more expensive. Configure 'object_detector' "
"under the llm utils config to use the intended detector."
)
llm = get_llm(ctx, name="operator")

raw_timeout = getattr(getattr(ctx, "llm_config", None), "timeout", None)
Expand Down
Loading
Loading