254 lines
7.4 KiB
TypeScript
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|