Files
leocrm/frontend/src/api/ai.ts
T

254 lines
7.4 KiB
TypeScript

/**
* AI Assistant plugin API client.
*
* All requests use the shared `apiClient` (`baseURL: '/api/v1'`) and target the
* AI Assistant plugin routes under `/ai/...`.
*/
import { apiDelete, apiGet, apiPost, apiPut } from './client';
// ─── Types ───
export interface AIProvider {
id: string;
name: string;
provider_type: string;
api_key: string;
base_url: string;
is_active: boolean;
is_default: boolean;
config: Record<string, unknown>;
created_at?: string;
updated_at?: string;
}
export interface AIModel {
id: string;
provider_id: string;
model_id: string;
display_name: string;
context_window: number;
supports_tools: boolean;
supports_streaming: boolean;
is_active: boolean;
config: Record<string, unknown>;
}
export interface AIPreset {
id: string;
name: string;
model_id: string;
provider_id: string | null;
temperature: number;
max_tokens: number;
top_p: number;
system_prompt: string;
config: Record<string, unknown>;
is_active: boolean;
}
export interface AIAgent {
id: string;
name: string;
description: string;
system_prompt: string;
preset_id: string | null;
tool_ids: string[];
is_default: boolean;
is_active: boolean;
config: Record<string, unknown>;
created_at?: string;
updated_at?: string;
}
export interface ChatFolder {
id: string;
name: string;
parent_id: string | null;
user_id: string;
created_at?: string;
}
export interface ChatSession {
id: string;
user_id: string;
agent_id: string | null;
title: string;
is_pinned: boolean;
is_sidebar: boolean;
folder_id: string | null;
created_at?: string;
updated_at?: string;
}
export interface ChatMessage {
id: string;
session_id: string;
role: string;
content: string;
tool_calls?: Record<string, unknown>[];
tool_results?: Record<string, unknown>[];
tokens: number;
model_used: string;
created_at?: string;
}
export interface ChatAttachment {
id: string;
message_id: string | null;
session_id: string;
filename: string;
mime_type: string;
size_bytes: number;
}
export interface AITool {
name: string;
description: string;
parameters: Record<string, unknown>;
plugin_name: string;
required_permission: string | null;
category: string;
}
// ─── Providers ───
export const fetchProviders = () => apiGet<AIProvider[]>('/ai/providers');
export const createProvider = (data: Partial<AIProvider>) => apiPost<AIProvider>('/ai/providers', data);
export const updateProvider = (id: string, data: Partial<AIProvider>) => apiPut<AIProvider>(`/ai/providers/${id}`, data);
export const deleteProvider = (id: string) => apiDelete(`/ai/providers/${id}`);
// ─── Models ───
export const fetchModels = (providerId?: string) =>
apiGet<AIModel[]>('/ai/models', { params: providerId ? { provider_id: providerId } : {} });
export const createModel = (data: Partial<AIModel>) => apiPost<AIModel>('/ai/models', data);
export const updateModel = (id: string, data: Partial<AIModel>) => apiPut<AIModel>(`/ai/models/${id}`, data);
export const deleteModel = (id: string) => apiDelete(`/ai/models/${id}`);
// ─── Presets ───
export const fetchPresets = () => apiGet<AIPreset[]>('/ai/presets');
export const createPreset = (data: Partial<AIPreset>) => apiPost<AIPreset>('/ai/presets', data);
export const updatePreset = (id: string, data: Partial<AIPreset>) => apiPut<AIPreset>(`/ai/presets/${id}`, data);
export const deletePreset = (id: string) => apiDelete(`/ai/presets/${id}`);
// ─── Agents ───
export const fetchAgents = () => apiGet<AIAgent[]>('/ai/agents');
export const createAgent = (data: Partial<AIAgent>) => apiPost<AIAgent>('/ai/agents', data);
export const updateAgent = (id: string, data: Partial<AIAgent>) => apiPut<AIAgent>(`/ai/agents/${id}`, data);
export const deleteAgent = (id: string) => apiDelete(`/ai/agents/${id}`);
// ─── Tools ───
export const fetchTools = async () => {
const res = await apiGet<AITool[] | { items: AITool[] }>('/ai/tools');
return Array.isArray(res) ? res : res.items ?? [];
};
// ─── Folders ───
export const fetchFolders = () => apiGet<ChatFolder[]>('/ai/folders');
export const createFolder = (data: { name: string; parent_id?: string }) => apiPost<ChatFolder>('/ai/folders', data);
export const updateFolder = (id: string, data: Partial<ChatFolder>) => apiPut<ChatFolder>(`/ai/folders/${id}`, data);
export const deleteFolder = (id: string) => apiDelete(`/ai/folders/${id}`);
// ─── Sessions ───
export const fetchSessions = (isSidebar?: boolean) =>
apiGet<ChatSession[]>('/ai/sessions', { params: isSidebar !== undefined ? { is_sidebar: isSidebar } : {} });
export const createSession = (data: { title?: string; agent_id?: string; is_sidebar?: boolean; folder_id?: string }) =>
apiPost<ChatSession>('/ai/sessions', data);
export const updateSession = (id: string, data: Partial<ChatSession>) =>
apiPut<ChatSession>(`/ai/sessions/${id}`, data);
export const deleteSession = (id: string) => apiDelete(`/ai/sessions/${id}`);
// ─── Messages ───
export const fetchMessages = (sessionId: string) =>
apiGet<ChatMessage[]>(`/ai/sessions/${sessionId}/messages`);
// ─── Attachments ───
export const fetchAttachments = (sessionId: string) =>
apiGet<ChatAttachment[]>(`/ai/sessions/${sessionId}/attachments`);
export async function uploadAttachment(sessionId: string, file: File): Promise<ChatAttachment> {
const formData = new FormData();
formData.append('file', file);
const csrfToken = sessionStorage.getItem('leocrm_csrf_token');
const response = await fetch(`/api/v1/ai/sessions/${sessionId}/attachments`, {
method: 'POST',
headers: {
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}),
},
credentials: 'include',
body: formData,
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
return response.json();
}
export function getAttachmentDownloadUrl(attachmentId: string): string {
return `/api/v1/ai/attachments/${attachmentId}/download`;
}
// ─── Streaming Chat ───
export interface StreamEvent {
type: 'token' | 'tool_calls' | 'tool_result' | 'done' | 'error';
content?: string;
tool?: string;
result?: string;
tools?: string[];
}
export async function* streamChat(
sessionId: string,
content: string,
agentId?: string
): AsyncGenerator<StreamEvent> {
const csrfToken = sessionStorage.getItem('leocrm_csrf_token');
const response = await fetch(`/api/v1/ai/sessions/${sessionId}/stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}),
},
credentials: 'include',
body: JSON.stringify({ content, agent_id: agentId }),
});
if (!response.ok) {
throw new Error(`Stream failed: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') return;
try {
yield JSON.parse(data);
} catch {
// skip invalid JSON
}
}
}
}
}