Files
OpenJarvis/frontend/src/lib/api.ts
T
2026-03-27 12:36:07 -07:00

626 lines
20 KiB
TypeScript

import type { ModelInfo, SavingsData, ServerInfo } from '../types';
// ---------------------------------------------------------------------------
// Supabase config — safe to embed (RLS protects writes)
// ---------------------------------------------------------------------------
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || 'https://mtbtgpwzrbostweaanpr.supabase.co';
const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im10YnRncHd6cmJvc3R3ZWFhbnByIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMxODk0OTQsImV4cCI6MjA4ODc2NTQ5NH0._xMlqCfljtXpwPj54H-ghxfLFO-jiq4W2WhpU8vVL1c';
declare global {
interface Window {
__TAURI_INTERNALS__?: unknown;
}
}
export const isTauri = () => typeof window !== 'undefined' && !!window.__TAURI_INTERNALS__;
// Cached API base URL fetched from the Tauri backend at startup.
// This avoids hardcoding the port — the Rust backend is the single
// source of truth for JARVIS_PORT.
let _tauriApiBase: string | null = null;
/** Pre-fetch the API base URL from the Tauri backend (call once at init). */
export async function initApiBase(): Promise<void> {
if (!isTauri()) return;
try {
const { invoke } = await import('@tauri-apps/api/core');
_tauriApiBase = await invoke<string>('get_api_base');
} catch {
// Command may not exist on older builds; fall through to default.
}
}
const DESKTOP_API_FALLBACK = 'http://127.0.0.1:8222';
const getSettingsApiUrl = (): string => {
try {
const raw = localStorage.getItem('openjarvis-settings');
if (raw) {
const parsed = JSON.parse(raw);
if (parsed.apiUrl) return parsed.apiUrl.replace(/\/+$/, '');
}
} catch {}
return '';
};
export const getBase = (): string => {
const settingsUrl = getSettingsApiUrl();
if (settingsUrl) return settingsUrl;
if (import.meta.env.VITE_API_URL) return import.meta.env.VITE_API_URL;
if (isTauri()) return _tauriApiBase || DESKTOP_API_FALLBACK;
return '';
};
async function tauriInvoke<T>(command: string, args: Record<string, unknown> = {}): Promise<T> {
const { invoke } = await import('@tauri-apps/api/core');
const apiUrl = getBase();
return invoke<T>(command, { apiUrl, ...args });
}
// ---------------------------------------------------------------------------
// Setup status (desktop only)
// ---------------------------------------------------------------------------
export interface SetupStatus {
phase: string;
detail: string;
ollama_ready: boolean;
server_ready: boolean;
model_ready: boolean;
error: string | null;
}
export async function getSetupStatus(): Promise<SetupStatus | null> {
if (!isTauri()) return null;
try {
const { invoke } = await import('@tauri-apps/api/core');
return await invoke<SetupStatus>('get_setup_status');
} catch {
return null;
}
}
// ---------------------------------------------------------------------------
// API functions
// ---------------------------------------------------------------------------
export async function fetchModels(): Promise<ModelInfo[]> {
if (isTauri()) {
try {
const result = await tauriInvoke<{ data?: ModelInfo[] }>('fetch_models');
return result?.data || [];
} catch {
// Fall through to fetch
}
}
const res = await fetch(`${getBase()}/v1/models`);
if (!res.ok) throw new Error(`Failed to fetch models: ${res.status}`);
const data = await res.json();
return data.data || [];
}
export async function fetchRecommendedModel(): Promise<{ model: string; reason: string }> {
const res = await fetch(`${getBase()}/v1/recommended-model`);
if (!res.ok) return { model: '', reason: 'Failed to fetch' };
return res.json();
}
export async function pullModel(modelName: string): Promise<void> {
// In Tauri, go through the Rust backend directly (avoids CORS / timeout
// issues with long model downloads via fetch).
if (isTauri()) {
try {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('pull_ollama_model', { modelName });
return;
} catch (e: any) {
throw new Error(e?.message || e || 'Download failed');
}
}
const res = await fetch(`${getBase()}/v1/models/pull`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: modelName }),
});
if (!res.ok) {
const detail = await res.text().catch(() => res.statusText);
throw new Error(`Failed to pull model: ${detail}`);
}
}
export async function deleteModel(modelName: string): Promise<void> {
if (isTauri()) {
try {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('delete_ollama_model', { modelName });
return;
} catch (e: any) {
throw new Error(e?.message || e || 'Delete failed');
}
}
const res = await fetch(`${getBase()}/v1/models/${encodeURIComponent(modelName)}`, {
method: 'DELETE',
});
if (!res.ok) {
const detail = await res.text().catch(() => res.statusText);
throw new Error(`Failed to delete model: ${detail}`);
}
}
const _CLOUD_PREFIXES = ['gpt-', 'o1-', 'o3-', 'o4-', 'claude-', 'gemini-', 'openrouter/'];
export async function preloadModel(modelName: string): Promise<void> {
// Cloud models don't need Ollama preloading
if (_CLOUD_PREFIXES.some(p => modelName.startsWith(p))) {
return;
}
// Trigger Ollama to load the model into memory (empty prompt, no generation).
const ollamaUrl = 'http://127.0.0.1:11434';
try {
const res = await fetch(`${ollamaUrl}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: modelName, prompt: '', keep_alive: '5m' }),
signal: AbortSignal.timeout(120_000),
});
if (!res.ok) throw new Error(`Preload failed: ${res.status}`);
} catch (e: any) {
if (e.name === 'TimeoutError') throw new Error('Model load timed out (120s)');
throw e;
}
}
export async function fetchSavings(): Promise<SavingsData> {
const res = await fetch(`${getBase()}/v1/savings`);
if (!res.ok) throw new Error(`Failed to fetch savings: ${res.status}`);
return res.json();
}
export async function fetchServerInfo(): Promise<ServerInfo> {
const res = await fetch(`${getBase()}/v1/info`);
if (!res.ok) throw new Error(`Failed to fetch server info: ${res.status}`);
return res.json();
}
export async function checkHealth(): Promise<boolean> {
if (isTauri()) {
try {
await tauriInvoke('check_health', { apiUrl: getBase() });
return true;
} catch {
return false;
}
}
try {
const res = await fetch(`${getBase()}/health`);
return res.ok;
} catch {
return false;
}
}
export async function fetchEnergy(): Promise<unknown> {
if (isTauri()) {
try {
return await tauriInvoke('fetch_energy', { apiUrl: getBase() });
} catch {}
}
const res = await fetch(`${getBase()}/v1/telemetry/energy`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
export async function fetchTelemetry(): Promise<unknown> {
if (isTauri()) {
try {
return await tauriInvoke('fetch_telemetry', { apiUrl: getBase() });
} catch {}
}
const res = await fetch(`${getBase()}/v1/telemetry/stats`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
export async function fetchTraces(limit: number = 50): Promise<unknown> {
if (isTauri()) {
try {
return await tauriInvoke('fetch_traces', { apiUrl: getBase(), limit });
} catch {}
}
const res = await fetch(`${getBase()}/v1/traces?limit=${limit}`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
// ---------------------------------------------------------------------------
// Speech
// ---------------------------------------------------------------------------
export interface TranscriptionResult {
text: string;
language: string | null;
confidence: number | null;
duration_seconds: number;
}
export interface SpeechHealth {
available: boolean;
backend?: string;
reason?: string;
}
export async function transcribeAudio(audioBlob: Blob, filename = 'recording.webm'): Promise<TranscriptionResult> {
if (isTauri()) {
try {
const buffer = await audioBlob.arrayBuffer();
return await tauriInvoke<TranscriptionResult>('transcribe_audio', {
audioData: Array.from(new Uint8Array(buffer)),
filename,
});
} catch {
// Fall through to fetch
}
}
const formData = new FormData();
formData.append('file', audioBlob, filename);
const res = await fetch(`${getBase()}/v1/speech/transcribe`, {
method: 'POST',
body: formData,
});
if (!res.ok) throw new Error(`Transcription failed: ${res.status}`);
return res.json();
}
export async function fetchSpeechHealth(): Promise<SpeechHealth> {
if (isTauri()) {
try {
return await tauriInvoke<SpeechHealth>('speech_health');
} catch {
return { available: false };
}
}
const res = await fetch(`${getBase()}/v1/speech/health`);
if (!res.ok) return { available: false };
return res.json();
}
// ---------------------------------------------------------------------------
// Agent Manager
// ---------------------------------------------------------------------------
export interface ManagedAgent {
id: string;
name: string;
agent_type: string;
config: Record<string, unknown>;
status: 'idle' | 'running' | 'paused' | 'error' | 'archived' | 'needs_attention' | 'budget_exceeded' | 'stalled';
summary_memory: string;
created_at: number;
updated_at: number;
// Runtime stats
total_runs?: number;
total_cost?: number;
total_tokens?: number;
input_tokens?: number;
output_tokens?: number;
last_run_at?: number | null;
// Schedule
schedule_type?: string;
schedule_value?: string;
// Budget
budget?: number;
// Learning
learning_enabled?: boolean;
// Live progress
current_activity?: string;
}
export interface AgentTask {
id: string;
agent_id: string;
description: string;
status: 'pending' | 'active' | 'completed' | 'failed';
progress: Record<string, unknown>;
findings: unknown[];
created_at: number;
}
export interface ChannelBinding {
id: string;
agent_id: string;
channel_type: string;
config: Record<string, unknown>;
session_id: string;
routing_mode: string;
}
export interface AgentTemplate {
id: string;
name: string;
description: string;
source: 'built-in' | 'user';
agent_type: string;
[key: string]: unknown;
}
export interface AgentMessage {
id: string;
agent_id: string;
direction: 'user_to_agent' | 'agent_to_user';
content: string;
mode: 'immediate' | 'queued';
status: 'pending' | 'delivered' | 'responded';
created_at: number;
}
export async function fetchManagedAgents(): Promise<ManagedAgent[]> {
const res = await fetch(`${getBase()}/v1/managed-agents`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const data = await res.json();
return data.agents || [];
}
export async function fetchManagedAgent(agentId: string): Promise<ManagedAgent> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
export async function createManagedAgent(body: {
name: string;
agent_type?: string;
template_id?: string;
config?: Record<string, unknown>;
}): Promise<ManagedAgent> {
const res = await fetch(`${getBase()}/v1/managed-agents`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
export async function updateManagedAgent(
agentId: string,
body: Partial<{ name: string; agent_type: string; config: Record<string, unknown> }>,
): Promise<ManagedAgent> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
export async function deleteManagedAgent(agentId: string): Promise<void> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`Failed: ${res.status}`);
}
export async function pauseManagedAgent(agentId: string): Promise<void> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/pause`, { method: 'POST' });
if (!res.ok) throw new Error(`Failed: ${res.status}`);
}
export async function resumeManagedAgent(agentId: string): Promise<void> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/resume`, { method: 'POST' });
if (!res.ok) throw new Error(`Failed: ${res.status}`);
}
export async function fetchAgentTasks(agentId: string): Promise<AgentTask[]> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/tasks`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const data = await res.json();
return data.tasks || [];
}
export async function createAgentTask(agentId: string, description: string): Promise<AgentTask> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description }),
});
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
export async function fetchAgentChannels(agentId: string): Promise<ChannelBinding[]> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/channels`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const data = await res.json();
return data.bindings || [];
}
export async function fetchTemplates(): Promise<AgentTemplate[]> {
const res = await fetch(`${getBase()}/v1/templates`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const data = await res.json();
return data.templates || [];
}
export async function runManagedAgent(agentId: string): Promise<void> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/run`, { method: 'POST' });
if (!res.ok) {
const body = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(body.detail || `Failed: ${res.status}`);
}
}
export async function recoverManagedAgent(agentId: string): Promise<{ recovered: boolean; checkpoint: unknown }> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/recover`, { method: 'POST' });
if (!res.ok) {
const body = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(body.detail || `Failed: ${res.status}`);
}
return res.json();
}
export async function fetchAgentState(agentId: string): Promise<{
agent: ManagedAgent;
tasks: AgentTask[];
channels: ChannelBinding[];
messages: AgentMessage[];
checkpoint: unknown;
}> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/state`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
export async function sendAgentMessage(agentId: string, content: string, mode: 'immediate' | 'queued' = 'queued'): Promise<AgentMessage> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, mode }),
});
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
export async function fetchAgentMessages(agentId: string): Promise<AgentMessage[]> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/messages`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const data = await res.json();
return data.messages || [];
}
export async function fetchErrorAgents(): Promise<ManagedAgent[]> {
const res = await fetch(`${getBase()}/v1/agents/errors`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const data = await res.json();
return data.agents || [];
}
// ---------------------------------------------------------------------------
// Agent Learning + Traces
// ---------------------------------------------------------------------------
export interface LearningLogEntry {
id: string;
agent_id: string;
event_type: string;
description: string;
data: Record<string, unknown>;
created_at: number;
}
export interface AgentTrace {
id: string;
outcome: string;
duration: number;
started_at: number;
steps: number;
error_message?: string;
metadata?: Record<string, unknown>;
}
export interface ToolInfo {
name: string;
description: string;
category: string;
source: 'tool' | 'channel';
requires_credentials: boolean;
credential_keys: string[];
configured: boolean;
}
export async function fetchAvailableTools(): Promise<ToolInfo[]> {
const res = await fetch(`${getBase()}/v1/tools`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const data = await res.json();
return data.tools || [];
}
export async function saveToolCredentials(
toolName: string,
credentials: Record<string, string>,
): Promise<void> {
const res = await fetch(`${getBase()}/v1/tools/${toolName}/credentials`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
if (!res.ok) throw new Error(`Failed: ${res.status}`);
}
export interface AgentTraceDetail {
id: string;
agent: string;
outcome: string;
duration: number;
started_at: number;
steps: Array<{
step_type: string;
input: unknown;
output: string;
duration: number;
metadata: Record<string, unknown>;
}>;
}
export async function fetchLearningLog(agentId: string): Promise<LearningLogEntry[]> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/learning`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const data = await res.json();
return data.learning_log || [];
}
export async function triggerLearning(agentId: string): Promise<void> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/learning/run`, { method: 'POST' });
if (!res.ok) throw new Error(`Failed: ${res.status}`);
}
export async function fetchAgentTraces(agentId: string, limit = 20): Promise<AgentTrace[]> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/traces?limit=${limit}`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const data = await res.json();
return data.traces || [];
}
export async function fetchAgentTrace(agentId: string, traceId: string): Promise<AgentTraceDetail> {
const res = await fetch(`${getBase()}/v1/managed-agents/${agentId}/traces/${traceId}`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
return res.json();
}
// ---------------------------------------------------------------------------
// Leaderboard savings submission (Supabase)
// ---------------------------------------------------------------------------
export interface SavingsSubmission {
anon_id: string;
display_name: string;
email: string;
total_calls: number;
total_tokens: number;
dollar_savings: number;
energy_wh_saved: number;
flops_saved: number;
token_counting_version?: number;
}
export async function submitSavings(data: SavingsSubmission): Promise<boolean> {
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) return false;
try {
const res = await fetch(
`${SUPABASE_URL}/rest/v1/savings_entries?on_conflict=anon_id`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
apikey: SUPABASE_ANON_KEY,
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
Prefer: 'resolution=merge-duplicates',
},
body: JSON.stringify(data),
},
);
return res.ok || res.status === 201 || res.status === 200;
} catch {
return false;
}
}