mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-08-17 02:12:00 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26bc7efb09 | ||
|
|
f3954e087a | ||
|
|
d865b4bed4 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "Git Clones",
|
||||
"message": "137,874",
|
||||
"message": "139,590",
|
||||
"color": "green",
|
||||
"namedLogo": "git"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"total_clones": 137874,
|
||||
"last_updated": "2026-06-30T07:21:14Z",
|
||||
"total_clones": 139590,
|
||||
"last_updated": "2026-07-01T07:33:10Z",
|
||||
"daily": {
|
||||
"2026-03-27": 2189,
|
||||
"2026-03-28": 1874,
|
||||
@@ -95,6 +95,8 @@
|
||||
"2026-06-25": 1640,
|
||||
"2026-06-26": 1338,
|
||||
"2026-06-27": 1338,
|
||||
"2026-06-28": 1028
|
||||
"2026-06-28": 1028,
|
||||
"2026-06-29": 765,
|
||||
"2026-06-30": 951
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ What it does:
|
||||
4. Installs `uv` (https://astral.sh/uv) if absent.
|
||||
5. Clones the OpenJarvis repository to `%LOCALAPPDATA%\OpenJarvis`
|
||||
(override with `$env:OPENJARVIS_HOME`).
|
||||
6. Runs `uv sync --extra desktop` so the FastAPI server and speech backend are
|
||||
importable.
|
||||
6. Runs `uv sync --extra desktop --group desktop-native` so the FastAPI server,
|
||||
speech backend, and native extension are importable.
|
||||
7. Optionally prompts to register a scheduled task that auto-starts the
|
||||
server at logon.
|
||||
|
||||
@@ -105,7 +105,7 @@ To pull the latest:
|
||||
```powershell
|
||||
cd "$env:LOCALAPPDATA\OpenJarvis\src"
|
||||
git pull --ff-only
|
||||
uv sync --extra desktop
|
||||
uv sync --extra desktop --group desktop-native
|
||||
```
|
||||
|
||||
Or re-run the installer with `-Force`:
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
4. Install uv (https://astral.sh/uv) if absent.
|
||||
5. Clone the OpenJarvis repository to $env:LOCALAPPDATA\OpenJarvis
|
||||
(override with $env:OPENJARVIS_HOME).
|
||||
6. Run `uv sync --extra desktop` so the FastAPI server and speech
|
||||
backend are importable.
|
||||
6. Run `uv sync --extra desktop --group desktop-native` so the FastAPI
|
||||
server, speech backend, and native extension are importable.
|
||||
7. Optionally register the scheduled-task service (see
|
||||
deploy/windows/jarvis-service.ps1).
|
||||
|
||||
@@ -279,13 +279,13 @@ if (Test-Path (Join-Path $srcDir '.git')) {
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. uv sync --extra desktop
|
||||
# 6. uv sync --extra desktop --group desktop-native
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Info "Running 'uv sync --extra desktop' in $srcDir (this can take a few minutes)..."
|
||||
Write-Info "Running 'uv sync --extra desktop --group desktop-native' in $srcDir (this can take a few minutes)..."
|
||||
Push-Location $srcDir
|
||||
try {
|
||||
& $uvExe sync --extra desktop
|
||||
& $uvExe sync --extra desktop --group desktop-native
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Fail "uv sync failed with exit code $LASTEXITCODE. Check the output above."
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ avoid a Linux VM; WSL2 remains the smoother experience for most users.
|
||||
## What you get
|
||||
|
||||
- A PowerShell installer that probes prerequisites, installs `uv`,
|
||||
clones the repo, and runs `uv sync --extra desktop`.
|
||||
clones the repo, and runs `uv sync --extra desktop --group desktop-native`.
|
||||
- An optional Windows scheduled-task service equivalent to the systemd
|
||||
unit and launchd plist.
|
||||
- Loopback default — the service binds `127.0.0.1` so no API key is
|
||||
@@ -38,7 +38,7 @@ The installer will:
|
||||
4. Install `uv` if absent (via the official `astral.sh/uv` PowerShell
|
||||
installer).
|
||||
5. Clone the repo to `%LOCALAPPDATA%\OpenJarvis\src`.
|
||||
6. Run `uv sync --extra desktop`.
|
||||
6. Run `uv sync --extra desktop --group desktop-native`.
|
||||
7. Prompt to register the scheduled-task service (skip with
|
||||
`-SkipService`).
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ use tokio::sync::Mutex;
|
||||
|
||||
const OLLAMA_PORT: u16 = 11434;
|
||||
const JARVIS_PORT: u16 = 8000;
|
||||
const DESKTOP_UV_SYNC_COMMAND: &str =
|
||||
"uv sync --extra desktop --extra inference-cloud --extra inference-google --group desktop-native";
|
||||
|
||||
/// Small, fast model used when startup needs a default Ollama tag.
|
||||
const STARTUP_MODEL: &str = "qwen3.5:4b";
|
||||
@@ -740,10 +742,11 @@ fn format_uv_sync_failure(
|
||||
format!(
|
||||
"`uv sync` failed in {} (exit {}). Last output:\n\n{}\n\n\
|
||||
Try opening a terminal in that directory and running \
|
||||
`uv sync --extra desktop` manually for the full output.{}",
|
||||
`{}` manually for the full output.{}",
|
||||
root.display(),
|
||||
code,
|
||||
tail,
|
||||
DESKTOP_UV_SYNC_COMMAND,
|
||||
rust_hint,
|
||||
)
|
||||
}
|
||||
@@ -829,7 +832,7 @@ fn format_extension_import_failure(root: &std::path::Path, stderr: &str) -> Stri
|
||||
"`openjarvis_rust` is still not importable after building. Last output:\n\n{}\n\n\
|
||||
Run these manually for the full build log:\n\n\
|
||||
cd {}\n\
|
||||
uv sync --extra desktop\n\
|
||||
{}\n\
|
||||
uv run python -c \"import openjarvis_rust\"",
|
||||
if tail.is_empty() {
|
||||
"(no stderr output)"
|
||||
@@ -837,6 +840,7 @@ fn format_extension_import_failure(root: &std::path::Path, stderr: &str) -> Stri
|
||||
&tail
|
||||
},
|
||||
root.display(),
|
||||
DESKTOP_UV_SYNC_COMMAND,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1350,6 +1354,9 @@ async fn boot_backend(backend: SharedBackend, status: SharedStatus) {
|
||||
"--extra", "desktop",
|
||||
"--extra", "inference-cloud",
|
||||
"--extra", "inference-google",
|
||||
// openjarvis_rust lives in a uv dependency group (not the published
|
||||
// `desktop` extra) so pip installs from PyPI don't require it (#584).
|
||||
"--group", "desktop-native",
|
||||
])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
@@ -2866,7 +2873,7 @@ mod tests {
|
||||
format_uv_sync_spawn_error, matching_installed_model, model_names_match, normalize_host,
|
||||
parse_inference_config, parse_ollama_model_names, preferred_installed_model,
|
||||
should_persist_resolved_model, startup_installed_model, upsert_engine_host,
|
||||
uv_sync_stderr_tail, InferenceConfig, SourceKind,
|
||||
uv_sync_stderr_tail, InferenceConfig, SourceKind, DESKTOP_UV_SYNC_COMMAND,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
@@ -2910,7 +2917,7 @@ mod tests {
|
||||
assert!(msg.contains("exit 2"));
|
||||
assert!(msg.contains("/home/u/.openjarvis/src"));
|
||||
assert!(msg.contains("failed to resolve numpy==2.1.3"));
|
||||
assert!(msg.contains("uv sync --extra desktop")); // actionable next step
|
||||
assert!(msg.contains(DESKTOP_UV_SYNC_COMMAND)); // actionable next step
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2962,7 +2969,7 @@ mod tests {
|
||||
"ModuleNotFoundError: No module named 'openjarvis_rust'",
|
||||
);
|
||||
assert!(msg.contains("openjarvis_rust"));
|
||||
assert!(msg.contains("uv sync --extra desktop"));
|
||||
assert!(msg.contains(DESKTOP_UV_SYNC_COMMAND));
|
||||
assert!(msg.contains("uv run python -c \"import openjarvis_rust\""));
|
||||
assert!(msg.contains("ModuleNotFoundError"));
|
||||
}
|
||||
|
||||
+9
-1
@@ -91,7 +91,6 @@ desktop = [
|
||||
"pydantic>=2.0",
|
||||
"python-multipart>=0.0.9",
|
||||
"faster-whisper>=1.0",
|
||||
"openjarvis-rust",
|
||||
]
|
||||
openhands = ["openhands-sdk>=1.0; python_version >= '3.12'"]
|
||||
gpu-metrics = ["pynvml>=12.0"]
|
||||
@@ -239,3 +238,12 @@ select = ["E", "F", "I", "W"]
|
||||
dev = [
|
||||
"maturin>=1.12.6",
|
||||
]
|
||||
# openjarvis_rust is the native PyO3 extension, built from the local Rust
|
||||
# workspace. It lives in a uv dependency group (PEP 735) — not the published
|
||||
# `desktop` extra — so `uv sync --group desktop-native` builds it from source
|
||||
# for the desktop app, while `pip install openjarvis[desktop]` from PyPI does
|
||||
# NOT try to resolve openjarvis-rust from PyPI, where it isn't published
|
||||
# (dependency groups are excluded from wheel metadata). See #584 / #615.
|
||||
desktop-native = [
|
||||
"openjarvis-rust",
|
||||
]
|
||||
|
||||
@@ -2263,14 +2263,14 @@ def create_agent_manager_router(
|
||||
import httpx
|
||||
|
||||
try:
|
||||
resp = httpx.get(
|
||||
"https://api.sendblue.co/api/lines",
|
||||
headers={
|
||||
"sb-api-key-id": api_key_id,
|
||||
"sb-api-secret-key": api_secret_key,
|
||||
},
|
||||
timeout=15.0,
|
||||
)
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.get(
|
||||
"https://api.sendblue.co/api/lines",
|
||||
headers={
|
||||
"sb-api-key-id": api_key_id,
|
||||
"sb-api-secret-key": api_secret_key,
|
||||
},
|
||||
)
|
||||
if resp.status_code == 401:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
@@ -2290,12 +2290,16 @@ def create_agent_manager_router(
|
||||
)
|
||||
numbers = []
|
||||
for line in lines:
|
||||
num = (
|
||||
line.get("number")
|
||||
or line.get("phone_number")
|
||||
or line.get("from_number")
|
||||
or (line if isinstance(line, str) else "")
|
||||
)
|
||||
if isinstance(line, str):
|
||||
num = line
|
||||
elif isinstance(line, dict):
|
||||
num = (
|
||||
line.get("number")
|
||||
or line.get("phone_number")
|
||||
or line.get("from_number")
|
||||
)
|
||||
else:
|
||||
num = None
|
||||
if num:
|
||||
numbers.append(num)
|
||||
return {
|
||||
@@ -2327,18 +2331,18 @@ def create_agent_manager_router(
|
||||
import httpx
|
||||
|
||||
try:
|
||||
resp = httpx.post(
|
||||
"https://api.sendblue.co/api/account/webhooks",
|
||||
headers={
|
||||
"sb-api-key-id": api_key_id,
|
||||
"sb-api-secret-key": api_secret_key,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"receive": webhook_url,
|
||||
},
|
||||
timeout=15.0,
|
||||
)
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(
|
||||
"https://api.sendblue.co/api/account/webhooks",
|
||||
headers={
|
||||
"sb-api-key-id": api_key_id,
|
||||
"sb-api-secret-key": api_secret_key,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"receive": webhook_url,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"registered": resp.status_code < 300,
|
||||
"status": resp.status_code,
|
||||
@@ -2378,16 +2382,16 @@ def create_agent_manager_router(
|
||||
if from_number:
|
||||
payload["from_number"] = from_number
|
||||
|
||||
resp = httpx.post(
|
||||
"https://api.sendblue.co/api/send-message",
|
||||
headers={
|
||||
"sb-api-key-id": api_key_id,
|
||||
"sb-api-secret-key": api_secret_key,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json=payload,
|
||||
timeout=15.0,
|
||||
)
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(
|
||||
"https://api.sendblue.co/api/send-message",
|
||||
headers={
|
||||
"sb-api-key-id": api_key_id,
|
||||
"sb-api-secret-key": api_secret_key,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json=payload,
|
||||
)
|
||||
return {
|
||||
"sent": resp.status_code < 300,
|
||||
"status": resp.status_code,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
@@ -894,7 +895,12 @@ async def transcribe_speech(request: Request):
|
||||
ext = filename.rsplit(".", 1)[-1] if "." in filename else "wav"
|
||||
|
||||
try:
|
||||
result = backend.transcribe(audio_bytes, format=ext, language=language or None)
|
||||
result = await asyncio.to_thread(
|
||||
backend.transcribe,
|
||||
audio_bytes,
|
||||
format=ext,
|
||||
language=language or None,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("Speech transcription failed")
|
||||
raise HTTPException(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any
|
||||
@@ -835,7 +836,7 @@ async def list_models(request: Request) -> ModelListResponse:
|
||||
# Filter out any cloud model IDs that may appear via MultiEngine.
|
||||
# Fall back to direct Ollama query only when the engine returns nothing.
|
||||
engine = request.app.state.engine
|
||||
all_ids = engine.list_models()
|
||||
all_ids = await asyncio.to_thread(engine.list_models)
|
||||
model_ids = [m for m in all_ids if not is_cloud_model(m)]
|
||||
if not model_ids:
|
||||
model_ids = await list_local_models()
|
||||
@@ -865,12 +866,12 @@ async def pull_model(request: Request):
|
||||
import httpx as _httpx
|
||||
|
||||
host = getattr(engine, "_host", "http://localhost:11434")
|
||||
client = _httpx.Client(base_url=host, timeout=600.0)
|
||||
try:
|
||||
resp = client.post(
|
||||
"/api/pull",
|
||||
json={"name": model_name, "stream": False},
|
||||
)
|
||||
async with _httpx.AsyncClient(base_url=host, timeout=600.0) as client:
|
||||
resp = await client.post(
|
||||
"/api/pull",
|
||||
json={"name": model_name, "stream": False},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
except (_httpx.ConnectError, _httpx.TimeoutException) as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Ollama unreachable: {exc}")
|
||||
@@ -879,8 +880,6 @@ async def pull_model(request: Request):
|
||||
status_code=exc.response.status_code,
|
||||
detail=f"Ollama error: {exc.response.text[:300]}",
|
||||
)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
return {"status": "ok", "model": model_name}
|
||||
|
||||
@@ -896,13 +895,13 @@ async def delete_model(model_name: str, request: Request):
|
||||
import httpx as _httpx
|
||||
|
||||
host = getattr(engine, "_host", "http://localhost:11434")
|
||||
client = _httpx.Client(base_url=host, timeout=30.0)
|
||||
try:
|
||||
resp = client.request(
|
||||
"DELETE",
|
||||
"/api/delete",
|
||||
json={"name": model_name},
|
||||
)
|
||||
async with _httpx.AsyncClient(base_url=host, timeout=30.0) as client:
|
||||
resp = await client.request(
|
||||
"DELETE",
|
||||
"/api/delete",
|
||||
json={"name": model_name},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
except (_httpx.ConnectError, _httpx.TimeoutException) as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Ollama unreachable: {exc}")
|
||||
@@ -911,8 +910,6 @@ async def delete_model(model_name: str, request: Request):
|
||||
status_code=exc.response.status_code,
|
||||
detail=f"Ollama error: {exc.response.text[:300]}",
|
||||
)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
return {"status": "deleted", "model": model_name}
|
||||
|
||||
|
||||
@@ -90,6 +90,9 @@ class TelemetryAggregator:
|
||||
def __init__(self, db_path: str | Path) -> None:
|
||||
self._db_path = str(db_path)
|
||||
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
||||
self._conn.execute("PRAGMA journal_mode=WAL")
|
||||
self._conn.execute("PRAGMA synchronous=NORMAL")
|
||||
self._conn.execute("PRAGMA busy_timeout=5000")
|
||||
self._conn.row_factory = sqlite3.Row
|
||||
|
||||
def _time_filter(
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -148,6 +149,10 @@ class TelemetryStore:
|
||||
def __init__(self, db_path: str | Path) -> None:
|
||||
self._db_path = str(db_path)
|
||||
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
||||
self._lock = threading.Lock()
|
||||
self._conn.execute("PRAGMA journal_mode=WAL")
|
||||
self._conn.execute("PRAGMA synchronous=NORMAL")
|
||||
self._conn.execute("PRAGMA busy_timeout=5000")
|
||||
self._conn.execute(_CREATE_TABLE)
|
||||
self._conn.execute(_CREATE_MINING_STATS_TABLE)
|
||||
self._conn.commit()
|
||||
@@ -166,53 +171,54 @@ class TelemetryStore:
|
||||
|
||||
def record(self, rec: TelemetryRecord) -> None:
|
||||
"""Persist a single telemetry record."""
|
||||
self._conn.execute(
|
||||
_INSERT,
|
||||
(
|
||||
rec.timestamp,
|
||||
rec.model_id,
|
||||
rec.engine,
|
||||
rec.agent,
|
||||
rec.prompt_tokens,
|
||||
rec.prompt_tokens_evaluated,
|
||||
rec.completion_tokens,
|
||||
rec.total_tokens,
|
||||
rec.latency_seconds,
|
||||
rec.ttft,
|
||||
rec.cost_usd,
|
||||
rec.energy_joules,
|
||||
rec.power_watts,
|
||||
rec.gpu_utilization_pct,
|
||||
rec.gpu_memory_used_gb,
|
||||
rec.gpu_temperature_c,
|
||||
rec.throughput_tok_per_sec,
|
||||
rec.prefill_latency_seconds,
|
||||
rec.decode_latency_seconds,
|
||||
rec.energy_method,
|
||||
rec.energy_vendor,
|
||||
rec.batch_id,
|
||||
1 if rec.is_warmup else 0,
|
||||
rec.cpu_energy_joules,
|
||||
rec.gpu_energy_joules,
|
||||
rec.dram_energy_joules,
|
||||
rec.tokens_per_joule,
|
||||
rec.energy_per_output_token_joules,
|
||||
rec.throughput_per_watt,
|
||||
rec.prefill_energy_joules,
|
||||
rec.decode_energy_joules,
|
||||
rec.mean_itl_ms,
|
||||
rec.median_itl_ms,
|
||||
rec.p90_itl_ms,
|
||||
rec.p95_itl_ms,
|
||||
rec.p99_itl_ms,
|
||||
rec.std_itl_ms,
|
||||
1 if rec.is_streaming else 0,
|
||||
rec.token_counting_version,
|
||||
rec.mining_session_id,
|
||||
json.dumps(rec.metadata),
|
||||
),
|
||||
)
|
||||
self._conn.commit()
|
||||
with self._lock:
|
||||
self._conn.execute(
|
||||
_INSERT,
|
||||
(
|
||||
rec.timestamp,
|
||||
rec.model_id,
|
||||
rec.engine,
|
||||
rec.agent,
|
||||
rec.prompt_tokens,
|
||||
rec.prompt_tokens_evaluated,
|
||||
rec.completion_tokens,
|
||||
rec.total_tokens,
|
||||
rec.latency_seconds,
|
||||
rec.ttft,
|
||||
rec.cost_usd,
|
||||
rec.energy_joules,
|
||||
rec.power_watts,
|
||||
rec.gpu_utilization_pct,
|
||||
rec.gpu_memory_used_gb,
|
||||
rec.gpu_temperature_c,
|
||||
rec.throughput_tok_per_sec,
|
||||
rec.prefill_latency_seconds,
|
||||
rec.decode_latency_seconds,
|
||||
rec.energy_method,
|
||||
rec.energy_vendor,
|
||||
rec.batch_id,
|
||||
1 if rec.is_warmup else 0,
|
||||
rec.cpu_energy_joules,
|
||||
rec.gpu_energy_joules,
|
||||
rec.dram_energy_joules,
|
||||
rec.tokens_per_joule,
|
||||
rec.energy_per_output_token_joules,
|
||||
rec.throughput_per_watt,
|
||||
rec.prefill_energy_joules,
|
||||
rec.decode_energy_joules,
|
||||
rec.mean_itl_ms,
|
||||
rec.median_itl_ms,
|
||||
rec.p90_itl_ms,
|
||||
rec.p95_itl_ms,
|
||||
rec.p99_itl_ms,
|
||||
rec.std_itl_ms,
|
||||
1 if rec.is_streaming else 0,
|
||||
rec.token_counting_version,
|
||||
rec.mining_session_id,
|
||||
json.dumps(rec.metadata),
|
||||
),
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def record_mining_stats(self, stats: Any) -> None:
|
||||
"""Persist one mining stats snapshot.
|
||||
@@ -220,28 +226,29 @@ class TelemetryStore:
|
||||
``stats`` is duck-typed to keep telemetry usable without importing the
|
||||
optional mining package at module import time.
|
||||
"""
|
||||
self._conn.execute(
|
||||
"""\
|
||||
with self._lock:
|
||||
self._conn.execute(
|
||||
"""\
|
||||
INSERT INTO mining_stats (
|
||||
recorded_at, provider_id, shares_submitted, shares_accepted, blocks_found,
|
||||
hashrate, uptime_seconds, last_share_at, last_error, payout_target, fees_owed
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
time.time(),
|
||||
stats.provider_id,
|
||||
stats.shares_submitted,
|
||||
stats.shares_accepted,
|
||||
stats.blocks_found,
|
||||
stats.hashrate,
|
||||
stats.uptime_seconds,
|
||||
stats.last_share_at,
|
||||
stats.last_error,
|
||||
stats.payout_target,
|
||||
stats.fees_owed,
|
||||
),
|
||||
)
|
||||
self._conn.commit()
|
||||
(
|
||||
time.time(),
|
||||
stats.provider_id,
|
||||
stats.shares_submitted,
|
||||
stats.shares_accepted,
|
||||
stats.blocks_found,
|
||||
stats.hashrate,
|
||||
stats.uptime_seconds,
|
||||
stats.last_share_at,
|
||||
stats.last_error,
|
||||
stats.payout_target,
|
||||
stats.fees_owed,
|
||||
),
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def list_recent(self, limit: int = 50) -> list[dict[str, Any]]:
|
||||
"""Return recent telemetry rows as dictionaries."""
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Guards for the openjarvis-rust packaging split (#584 / #615).
|
||||
|
||||
``openjarvis_rust`` is the native PyO3 extension. It is NOT published to PyPI,
|
||||
so it must not appear in the published ``desktop`` extra — listing it there
|
||||
breaks ``pip install openjarvis[desktop]`` at install time. It lives in the uv
|
||||
``desktop-native`` dependency group instead (excluded from wheel metadata),
|
||||
which the desktop app installs from source via
|
||||
``uv sync --group desktop-native``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
PYPROJECT = ROOT / "pyproject.toml"
|
||||
DESKTOP_LIB_RS = ROOT / "frontend" / "src-tauri" / "src" / "lib.rs"
|
||||
WINDOWS_INSTALL_PS1 = ROOT / "deploy" / "windows" / "install.ps1"
|
||||
|
||||
|
||||
def _pyproject() -> dict:
|
||||
return tomllib.loads(PYPROJECT.read_text())
|
||||
|
||||
|
||||
def test_openjarvis_rust_not_in_published_desktop_extra() -> None:
|
||||
desktop = _pyproject()["project"]["optional-dependencies"]["desktop"]
|
||||
assert not any("openjarvis-rust" in dep for dep in desktop), (
|
||||
"openjarvis-rust must not be in the published `desktop` extra — it is "
|
||||
"not on PyPI, so it breaks `pip install openjarvis[desktop]`."
|
||||
)
|
||||
|
||||
|
||||
def test_openjarvis_rust_lives_in_uv_dependency_group() -> None:
|
||||
group = _pyproject()["dependency-groups"]["desktop-native"]
|
||||
assert any("openjarvis-rust" in dep for dep in group)
|
||||
|
||||
|
||||
def test_openjarvis_rust_has_local_uv_path_source() -> None:
|
||||
src = _pyproject()["tool"]["uv"]["sources"]["openjarvis-rust"]
|
||||
assert src["path"] == "rust/crates/openjarvis-python"
|
||||
|
||||
|
||||
def test_desktop_app_syncs_the_native_group() -> None:
|
||||
# Otherwise the group's openjarvis_rust is never installed for the app.
|
||||
assert '"desktop-native"' in DESKTOP_LIB_RS.read_text(), (
|
||||
"the desktop app must `uv sync --group desktop-native` so the native "
|
||||
"extension is built at launch."
|
||||
)
|
||||
|
||||
|
||||
def test_windows_installer_syncs_the_native_group() -> None:
|
||||
# The Windows source installer does not run maturin separately.
|
||||
assert (
|
||||
"& $uvExe sync --extra desktop --group desktop-native"
|
||||
in WINDOWS_INSTALL_PS1.read_text()
|
||||
), (
|
||||
"the Windows installer must include `--group desktop-native` so "
|
||||
"openjarvis_rust is built during source install."
|
||||
)
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -48,6 +48,71 @@ class TestAgentManagerRoutes:
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["agents"] == []
|
||||
|
||||
def test_sendblue_verify_uses_async_http_client(self, client):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.json.return_value = [
|
||||
"+15551234567",
|
||||
{"phone_number": "+15557654321"},
|
||||
None,
|
||||
]
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client_cls:
|
||||
instance = mock_client_cls.return_value.__aenter__.return_value
|
||||
instance.get = AsyncMock(return_value=mock_resp)
|
||||
resp = client.post(
|
||||
"/v1/channels/sendblue/verify",
|
||||
json={"api_key_id": "key-id", "api_secret_key": "secret"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["valid"] is True
|
||||
assert resp.json()["numbers"] == ["+15551234567", "+15557654321"]
|
||||
instance.get.assert_awaited_once()
|
||||
|
||||
def test_sendblue_register_webhook_uses_async_http_client(self, client):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.json.return_value = {"ok": True}
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client_cls:
|
||||
instance = mock_client_cls.return_value.__aenter__.return_value
|
||||
instance.post = AsyncMock(return_value=mock_resp)
|
||||
resp = client.post(
|
||||
"/v1/channels/sendblue/register-webhook",
|
||||
json={
|
||||
"api_key_id": "key-id",
|
||||
"api_secret_key": "secret",
|
||||
"webhook_url": "https://example.com/webhooks/sendblue",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["registered"] is True
|
||||
instance.post.assert_awaited_once()
|
||||
|
||||
def test_sendblue_test_message_uses_async_http_client(self, client):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.json.return_value = {"ok": True}
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client_cls:
|
||||
instance = mock_client_cls.return_value.__aenter__.return_value
|
||||
instance.post = AsyncMock(return_value=mock_resp)
|
||||
resp = client.post(
|
||||
"/v1/channels/sendblue/test",
|
||||
json={
|
||||
"api_key_id": "key-id",
|
||||
"api_secret_key": "secret",
|
||||
"from_number": "+15550000000",
|
||||
"to_number": "+15551234567",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["sent"] is True
|
||||
instance.post.assert_awaited_once()
|
||||
|
||||
def test_create_agent(self, client):
|
||||
resp = client.post(
|
||||
"/v1/managed-agents",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -75,10 +75,9 @@ class TestModelPull:
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
|
||||
with patch("httpx.Client") as MockClient:
|
||||
instance = MockClient.return_value
|
||||
instance.post.return_value = mock_resp
|
||||
instance.close = MagicMock()
|
||||
with patch("httpx.AsyncClient") as MockClient:
|
||||
instance = MockClient.return_value.__aenter__.return_value
|
||||
instance.post = AsyncMock(return_value=mock_resp)
|
||||
|
||||
resp = client.post("/v1/models/pull", json={"model": "qwen3.5:4b"})
|
||||
|
||||
@@ -86,6 +85,10 @@ class TestModelPull:
|
||||
data = resp.json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["model"] == "qwen3.5:4b"
|
||||
instance.post.assert_awaited_once_with(
|
||||
"/api/pull",
|
||||
json={"name": "qwen3.5:4b", "stream": False},
|
||||
)
|
||||
|
||||
def test_pull_ollama_unreachable(self):
|
||||
engine = _make_ollama_engine()
|
||||
@@ -93,10 +96,9 @@ class TestModelPull:
|
||||
|
||||
import httpx
|
||||
|
||||
with patch("httpx.Client") as MockClient:
|
||||
instance = MockClient.return_value
|
||||
instance.post.side_effect = httpx.ConnectError("refused")
|
||||
instance.close = MagicMock()
|
||||
with patch("httpx.AsyncClient") as MockClient:
|
||||
instance = MockClient.return_value.__aenter__.return_value
|
||||
instance.post = AsyncMock(side_effect=httpx.ConnectError("refused"))
|
||||
|
||||
resp = client.post("/v1/models/pull", json={"model": "foo"})
|
||||
|
||||
@@ -123,10 +125,9 @@ class TestModelDelete:
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
|
||||
with patch("httpx.Client") as MockClient:
|
||||
instance = MockClient.return_value
|
||||
instance.request.return_value = mock_resp
|
||||
instance.close = MagicMock()
|
||||
with patch("httpx.AsyncClient") as MockClient:
|
||||
instance = MockClient.return_value.__aenter__.return_value
|
||||
instance.request = AsyncMock(return_value=mock_resp)
|
||||
|
||||
resp = client.delete("/v1/models/qwen3:0.6b")
|
||||
|
||||
@@ -134,6 +135,11 @@ class TestModelDelete:
|
||||
data = resp.json()
|
||||
assert data["status"] == "deleted"
|
||||
assert data["model"] == "qwen3:0.6b"
|
||||
instance.request.assert_awaited_once_with(
|
||||
"DELETE",
|
||||
"/api/delete",
|
||||
json={"name": "qwen3:0.6b"},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -268,3 +274,19 @@ class TestModelsEndpointExtended:
|
||||
assert resp.status_code == 200
|
||||
# The endpoint returns whatever list_models() gives
|
||||
assert resp.json()["object"] == "list"
|
||||
|
||||
def test_models_list_offloads_engine_list_models(self):
|
||||
engine = _make_engine(models=["qwen3.5:4b"])
|
||||
app = create_app(engine, "qwen3.5:4b")
|
||||
client = TestClient(app)
|
||||
|
||||
with patch(
|
||||
"openjarvis.server.routes.asyncio.to_thread",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_to_thread:
|
||||
mock_to_thread.return_value = ["qwen3.5:4b"]
|
||||
resp = client.get("/v1/models")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert [m["id"] for m in resp.json()["data"]] == ["qwen3.5:4b"]
|
||||
mock_to_thread.assert_awaited_once_with(engine.list_models)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for speech API endpoints."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -56,6 +56,33 @@ def test_transcribe_endpoint(client, mock_speech_backend):
|
||||
assert data["duration_seconds"] == 1.5
|
||||
|
||||
|
||||
def test_transcribe_endpoint_offloads_backend_work(client, mock_speech_backend):
|
||||
expected = TranscriptionResult(
|
||||
text="Offloaded",
|
||||
language="en",
|
||||
confidence=0.9,
|
||||
duration_seconds=1.0,
|
||||
segments=[],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"openjarvis.server.api_routes.asyncio.to_thread",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_to_thread:
|
||||
mock_to_thread.return_value = expected
|
||||
response = client.post(
|
||||
"/v1/speech/transcribe",
|
||||
files={"file": ("test.wav", b"fake audio data", "audio/wav")},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_to_thread.assert_awaited_once()
|
||||
args, kwargs = mock_to_thread.await_args
|
||||
assert args == (mock_speech_backend.transcribe, b"fake audio data")
|
||||
assert kwargs == {"format": "wav", "language": None}
|
||||
assert response.json()["text"] == "Offloaded"
|
||||
|
||||
|
||||
def test_transcribe_endpoint_surfaces_backend_error(client, mock_speech_backend):
|
||||
mock_speech_backend.transcribe.side_effect = RuntimeError("missing cublas64_12.dll")
|
||||
|
||||
|
||||
@@ -49,6 +49,21 @@ def _setup(tmp_path: Path, records: list[TelemetryRecord] | None = None):
|
||||
|
||||
|
||||
class TestTelemetryAggregator:
|
||||
def test_uses_wal_with_normal_synchronous_and_busy_timeout(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
agg = _setup(tmp_path)
|
||||
|
||||
journal_mode = agg._conn.execute("PRAGMA journal_mode").fetchone()[0]
|
||||
synchronous = agg._conn.execute("PRAGMA synchronous").fetchone()[0]
|
||||
busy_timeout = agg._conn.execute("PRAGMA busy_timeout").fetchone()[0]
|
||||
|
||||
assert journal_mode.lower() == "wal"
|
||||
assert synchronous == 1
|
||||
assert busy_timeout == 5000
|
||||
agg.close()
|
||||
|
||||
def test_empty_db_summary(self, tmp_path: Path) -> None:
|
||||
agg = _setup(tmp_path)
|
||||
s = agg.summary()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
from openjarvis.core.events import EventBus, EventType
|
||||
@@ -17,6 +18,35 @@ class TestTelemetryStore:
|
||||
assert rows == []
|
||||
store.close()
|
||||
|
||||
def test_uses_wal_with_normal_synchronous(self, tmp_path: Path) -> None:
|
||||
store = TelemetryStore(tmp_path / "test.db")
|
||||
journal_mode = store._conn.execute("PRAGMA journal_mode").fetchone()[0]
|
||||
synchronous = store._conn.execute("PRAGMA synchronous").fetchone()[0]
|
||||
busy_timeout = store._conn.execute("PRAGMA busy_timeout").fetchone()[0]
|
||||
|
||||
assert journal_mode.lower() == "wal"
|
||||
assert synchronous == 1
|
||||
assert busy_timeout == 5000
|
||||
store.close()
|
||||
|
||||
def test_concurrent_record_writes_are_serialized(self, tmp_path: Path) -> None:
|
||||
store = TelemetryStore(tmp_path / "test.db")
|
||||
|
||||
def write_one(i: int) -> None:
|
||||
store.record(
|
||||
TelemetryRecord(
|
||||
timestamp=time.time(),
|
||||
model_id=f"model-{i}",
|
||||
engine="test",
|
||||
)
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
list(pool.map(write_one, range(32)))
|
||||
|
||||
assert len(store._fetchall()) == 32
|
||||
store.close()
|
||||
|
||||
def test_record_values(self, tmp_path: Path) -> None:
|
||||
store = TelemetryStore(tmp_path / "test.db")
|
||||
rec = TelemetryRecord(
|
||||
|
||||
@@ -4206,7 +4206,6 @@ dashboard = [
|
||||
desktop = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "faster-whisper" },
|
||||
{ name = "openjarvis-rust" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "uvicorn" },
|
||||
@@ -4339,6 +4338,9 @@ tools-search = [
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
desktop-native = [
|
||||
{ name = "openjarvis-rust" },
|
||||
]
|
||||
dev = [
|
||||
{ name = "maturin" },
|
||||
]
|
||||
@@ -4391,7 +4393,6 @@ requires-dist = [
|
||||
{ name = "openai", marker = "extra == 'inference-cloud'", specifier = ">=1.30" },
|
||||
{ name = "openai", marker = "extra == 'media'", specifier = ">=1.30" },
|
||||
{ name = "openhands-sdk", marker = "python_full_version >= '3.12' and extra == 'openhands'", specifier = ">=1.0" },
|
||||
{ name = "openjarvis-rust", marker = "extra == 'desktop'", directory = "rust/crates/openjarvis-python" },
|
||||
{ name = "pdfplumber", marker = "extra == 'memory-pdf'", specifier = ">=0.10" },
|
||||
{ name = "pdfplumber", marker = "extra == 'pdf'", specifier = ">=0.10" },
|
||||
{ name = "playwright", marker = "extra == 'browser'", specifier = ">=1.40" },
|
||||
@@ -4445,6 +4446,7 @@ requires-dist = [
|
||||
provides-extras = ["browser", "channel-discord", "channel-gmail", "channel-line", "channel-mastodon", "channel-messenger", "channel-nostr", "channel-reddit", "channel-rocketchat", "channel-slack", "channel-telegram", "channel-twilio", "channel-twitch", "channel-twitter", "channel-viber", "channel-xmpp", "channel-zulip", "dashboard", "desktop", "dev", "docs", "energy-all", "energy-amd", "energy-apple", "eval-sheets", "eval-wandb", "framework-comparison", "gpu-metrics", "inference-cloud", "inference-gemma", "inference-google", "inference-litellm", "inference-mlx", "inference-vllm", "learning-dspy", "learning-gepa", "media", "memory-bm25", "memory-colbert", "memory-faiss", "memory-pdf", "mining-pearl-cpu", "mining-pearl-vllm", "openhands", "orchestrator-training", "pdf", "sandbox-docker", "sandbox-wasm", "scheduler", "security-signing", "server", "speech", "speech-deepgram", "tools-search"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
desktop-native = [{ name = "openjarvis-rust", directory = "rust/crates/openjarvis-python" }]
|
||||
dev = [{ name = "maturin", specifier = ">=1.12.6" }]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user