7f61dfb25b
Check Cross-Plugin Imports / check (push) Has been cancelled
Migration 0137: Drop AI chat tables (ai_chat_sessions, ai_chat_messages, ai_chat_attachments, ai_conversations, ai_messages)
Backend:
- Remove AIChatSession, AIChatMessage, AIChatAttachment models from ai_assistant/models.py
- Remove AIConversation, AIMessage from app/models/__init__.py
- Remove session/message/stream/attachment routes from ai_assistant/routes.py
- Add new streaming route POST /ai/conversations/{conversation_id}/stream using comm tables
- Add new messages route GET /ai/conversations/{conversation_id}/messages using comm tables
- Add stream_chat_comm, get_comm_messages, save_comm_message to services.py
- Update external_api.py to use CommConversation/CommMessage instead of AIChatSession/AIChatMessage
- Update unified_search ai_chat_provider to search comm_messages with conversation_type=ai
- Remove ai_copilot router from main.py and routes/__init__.py
- Remove ai_conversation from entity_permissions.py and owner_transfer_service.py
- Update ai_assistant/plugin.py get_entity_models to remove AIChatSession
- Guard ai_copilot_service.py imports with try/except
Frontend:
- Remove AIAssistant.tsx, AIAssistantStandalone.tsx, SessionList.tsx, ChatWindow.tsx
- Remove AI Assistant routes from routes/index.tsx
- Update api/ai.ts: streamChat uses /ai/conversations/{id}/stream, fetchMessages uses /ai/conversations/{id}/messages
- Update Communication.tsx: use convId for AI streaming, remove aiSessionId, use fetchAiMessages for AI conversations
- Update AiChatPanel.tsx: create comm conversation instead of AI session, use new fetchMessages
- Update AISidebar.tsx: remove ChatWindow import, show placeholder
tsc clean, build successful, backend import OK
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 = (conversationId: string) =>
|
|
apiGet<{ role: string; content: string }[]>(`/ai/conversations/${conversationId}/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(
|
|
conversationId: string,
|
|
content: string,
|
|
agentId?: string
|
|
): AsyncGenerator<StreamEvent> {
|
|
const csrfToken = sessionStorage.getItem('leocrm_csrf_token');
|
|
const response = await fetch(`/api/v1/ai/conversations/${conversationId}/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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|