@chimerai/cli 0.2.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +317 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +2126 -0
- package/dist/commands/create.d.ts +12 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +1703 -0
- package/dist/commands/deploy.d.ts +11 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +219 -0
- package/dist/commands/dev.d.ts +17 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +206 -0
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +728 -0
- package/dist/commands/generate.d.ts +19 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +429 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +269 -0
- package/dist/commands/list.d.ts +12 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +328 -0
- package/dist/commands/migrate.d.ts +14 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +197 -0
- package/dist/commands/plugin.d.ts +10 -0
- package/dist/commands/plugin.d.ts.map +1 -0
- package/dist/commands/plugin.js +239 -0
- package/dist/commands/remove.d.ts +11 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +472 -0
- package/dist/commands/secret.d.ts +12 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +102 -0
- package/dist/commands/setup.d.ts +9 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +788 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +211 -0
- package/dist/commands/use.d.ts +9 -0
- package/dist/commands/use.d.ts.map +1 -0
- package/dist/commands/use.js +51 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/license.d.ts +55 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +258 -0
- package/dist/scanner.d.ts +31 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +113 -0
- package/dist/schema-manager.d.ts +26 -0
- package/dist/schema-manager.d.ts.map +1 -0
- package/dist/schema-manager.js +132 -0
- package/dist/templates/admin.d.ts +49 -0
- package/dist/templates/admin.d.ts.map +1 -0
- package/dist/templates/admin.js +1358 -0
- package/dist/templates/ai-routes.d.ts +17 -0
- package/dist/templates/ai-routes.d.ts.map +1 -0
- package/dist/templates/ai-routes.js +1130 -0
- package/dist/templates/ai-service-tools.d.ts +22 -0
- package/dist/templates/ai-service-tools.d.ts.map +1 -0
- package/dist/templates/ai-service-tools.js +1424 -0
- package/dist/templates/ai-service.d.ts +66 -0
- package/dist/templates/ai-service.d.ts.map +1 -0
- package/dist/templates/ai-service.js +2202 -0
- package/dist/templates/api-routes.d.ts +108 -0
- package/dist/templates/api-routes.d.ts.map +1 -0
- package/dist/templates/api-routes.js +1219 -0
- package/dist/templates/auth.d.ts +48 -0
- package/dist/templates/auth.d.ts.map +1 -0
- package/dist/templates/auth.js +381 -0
- package/dist/templates/billing.d.ts +44 -0
- package/dist/templates/billing.d.ts.map +1 -0
- package/dist/templates/billing.js +551 -0
- package/dist/templates/chat.d.ts +63 -0
- package/dist/templates/chat.d.ts.map +1 -0
- package/dist/templates/chat.js +1979 -0
- package/dist/templates/components.d.ts +22 -0
- package/dist/templates/components.d.ts.map +1 -0
- package/dist/templates/components.js +672 -0
- package/dist/templates/config.d.ts +6 -0
- package/dist/templates/config.d.ts.map +1 -0
- package/dist/templates/config.js +86 -0
- package/dist/templates/docker.d.ts +25 -0
- package/dist/templates/docker.d.ts.map +1 -0
- package/dist/templates/docker.js +165 -0
- package/dist/templates/gdpr.d.ts +16 -0
- package/dist/templates/gdpr.d.ts.map +1 -0
- package/dist/templates/gdpr.js +259 -0
- package/dist/templates/index.d.ts +77 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +339 -0
- package/dist/templates/layout.d.ts +67 -0
- package/dist/templates/layout.d.ts.map +1 -0
- package/dist/templates/layout.js +670 -0
- package/dist/templates/mfa.d.ts +23 -0
- package/dist/templates/mfa.d.ts.map +1 -0
- package/dist/templates/mfa.js +353 -0
- package/dist/templates/middleware.d.ts +12 -0
- package/dist/templates/middleware.d.ts.map +1 -0
- package/dist/templates/middleware.js +116 -0
- package/dist/templates/prisma.d.ts +35 -0
- package/dist/templates/prisma.d.ts.map +1 -0
- package/dist/templates/prisma.js +724 -0
- package/dist/templates/provider-routes.d.ts +21 -0
- package/dist/templates/provider-routes.d.ts.map +1 -0
- package/dist/templates/provider-routes.js +1203 -0
- package/dist/templates/rag.d.ts +48 -0
- package/dist/templates/rag.d.ts.map +1 -0
- package/dist/templates/rag.js +532 -0
- package/dist/templates/widget.d.ts +64 -0
- package/dist/templates/widget.d.ts.map +1 -0
- package/dist/templates/widget.js +1360 -0
- package/dist/utils/provider-db.d.ts +63 -0
- package/dist/utils/provider-db.d.ts.map +1 -0
- package/dist/utils/provider-db.js +300 -0
- package/dist/utils.d.ts +78 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +330 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AI Service Route Template Generators
|
|
4
|
+
*
|
|
5
|
+
* Generates Python route files for the modular AI-Service.
|
|
6
|
+
* Each module (chat, rag, guardrails, tools) gets its own route file
|
|
7
|
+
* under services/ai/routes/.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.generateChatRoutes = generateChatRoutes;
|
|
11
|
+
exports.generateRagRoutes = generateRagRoutes;
|
|
12
|
+
exports.generateGuardrailsRoutes = generateGuardrailsRoutes;
|
|
13
|
+
exports.generateToolsRoutes = generateToolsRoutes;
|
|
14
|
+
exports.generateRoutesInit = generateRoutesInit;
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Chat Routes — /api/chat, /api/chat/stream, /api/models, /api/moderate
|
|
17
|
+
// ============================================================================
|
|
18
|
+
function generateChatRoutes() {
|
|
19
|
+
return `"""Chat API Routes — Auto-generated by ChimerAI CLI."""
|
|
20
|
+
|
|
21
|
+
from fastapi import APIRouter, HTTPException
|
|
22
|
+
from fastapi.responses import StreamingResponse
|
|
23
|
+
import structlog
|
|
24
|
+
|
|
25
|
+
from models import (
|
|
26
|
+
ChatCompletionRequest,
|
|
27
|
+
ChatCompletionResponse,
|
|
28
|
+
ModelsResponse,
|
|
29
|
+
ModerationRequest,
|
|
30
|
+
ModerationResponse,
|
|
31
|
+
)
|
|
32
|
+
from services.chat_service import chat_service
|
|
33
|
+
from services.model_service import model_service
|
|
34
|
+
from services.moderation_service import moderation_service
|
|
35
|
+
|
|
36
|
+
logger = structlog.get_logger()
|
|
37
|
+
|
|
38
|
+
router = APIRouter(prefix="/api", tags=["chat"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.post("/chat", response_model=ChatCompletionResponse)
|
|
42
|
+
async def chat_completion(request: ChatCompletionRequest):
|
|
43
|
+
"""Non-streaming chat completion."""
|
|
44
|
+
try:
|
|
45
|
+
return await chat_service.create_completion(
|
|
46
|
+
request,
|
|
47
|
+
provider_id=request.provider_id,
|
|
48
|
+
user_id=request.user_id,
|
|
49
|
+
)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error("chat_completion_error", error=str(e))
|
|
52
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.post("/chat/stream")
|
|
56
|
+
async def chat_stream(request: ChatCompletionRequest):
|
|
57
|
+
"""Streaming chat completion via SSE."""
|
|
58
|
+
return StreamingResponse(
|
|
59
|
+
chat_service.create_streaming_completion(
|
|
60
|
+
request,
|
|
61
|
+
provider_id=request.provider_id,
|
|
62
|
+
user_id=request.user_id,
|
|
63
|
+
),
|
|
64
|
+
media_type="text/event-stream",
|
|
65
|
+
headers={
|
|
66
|
+
"Cache-Control": "no-cache",
|
|
67
|
+
"Connection": "keep-alive",
|
|
68
|
+
"X-Accel-Buffering": "no",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.get("/models", response_model=ModelsResponse)
|
|
74
|
+
async def list_models():
|
|
75
|
+
"""List available AI models."""
|
|
76
|
+
try:
|
|
77
|
+
return await model_service.list_models()
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error("list_models_error", error=str(e))
|
|
80
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.post("/moderate", response_model=ModerationResponse)
|
|
84
|
+
async def moderate_content(request: ModerationRequest):
|
|
85
|
+
"""Content moderation check."""
|
|
86
|
+
try:
|
|
87
|
+
return await moderation_service.moderate_content(
|
|
88
|
+
request,
|
|
89
|
+
provider_id=request.provider_id,
|
|
90
|
+
user_id=request.user_id,
|
|
91
|
+
)
|
|
92
|
+
except ValueError as e:
|
|
93
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error("moderation_error", error=str(e))
|
|
96
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// RAG Routes — /api/rag/*, /api/embeddings
|
|
101
|
+
// ============================================================================
|
|
102
|
+
function generateRagRoutes() {
|
|
103
|
+
return `"""RAG API Routes — Auto-generated by ChimerAI CLI."""
|
|
104
|
+
|
|
105
|
+
from typing import List, Optional
|
|
106
|
+
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
|
107
|
+
import structlog
|
|
108
|
+
|
|
109
|
+
from models import (
|
|
110
|
+
AddDocumentsRequest,
|
|
111
|
+
SearchRequest,
|
|
112
|
+
RAGChatRequest,
|
|
113
|
+
EmbeddingRequest,
|
|
114
|
+
EmbeddingResponse,
|
|
115
|
+
SearchResponse,
|
|
116
|
+
VectorStoreStats,
|
|
117
|
+
DeleteDocumentsRequest,
|
|
118
|
+
DeleteDocumentsResponse,
|
|
119
|
+
)
|
|
120
|
+
from services.rag_service import rag_service
|
|
121
|
+
from services.embedding_service import embedding_service
|
|
122
|
+
from services.file_extractor import extract_text, get_supported_formats
|
|
123
|
+
|
|
124
|
+
logger = structlog.get_logger()
|
|
125
|
+
|
|
126
|
+
router = APIRouter(prefix="/api", tags=["rag"])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@router.post("/rag/upload")
|
|
130
|
+
async def upload_files(files: List[UploadFile] = File(...)):
|
|
131
|
+
"""Upload files (PDF, DOCX, TXT, MD, etc.) to the vector store.
|
|
132
|
+
|
|
133
|
+
Files are automatically parsed — text is extracted from PDF/DOCX,
|
|
134
|
+
other files are read as UTF-8 text. Each file is chunked and embedded.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
total_added = 0
|
|
138
|
+
all_ids = []
|
|
139
|
+
results = []
|
|
140
|
+
|
|
141
|
+
for file in files:
|
|
142
|
+
try:
|
|
143
|
+
file_bytes = await file.read()
|
|
144
|
+
filename = file.filename or "unknown.txt"
|
|
145
|
+
text = extract_text(file_bytes, filename)
|
|
146
|
+
|
|
147
|
+
result = await rag_service.add_documents(
|
|
148
|
+
documents=[text],
|
|
149
|
+
metadatas=[{"filename": filename, "content_type": file.content_type or "unknown"}],
|
|
150
|
+
)
|
|
151
|
+
total_added += result["added"]
|
|
152
|
+
all_ids.extend(result["ids"])
|
|
153
|
+
results.append({
|
|
154
|
+
"filename": filename,
|
|
155
|
+
"status": "success",
|
|
156
|
+
"chunks": result["added"],
|
|
157
|
+
})
|
|
158
|
+
logger.info("file_uploaded", filename=filename, chunks=result["added"])
|
|
159
|
+
except ValueError as ve:
|
|
160
|
+
results.append({
|
|
161
|
+
"filename": file.filename or "unknown",
|
|
162
|
+
"status": "error",
|
|
163
|
+
"error": str(ve),
|
|
164
|
+
})
|
|
165
|
+
logger.warning("file_upload_failed", filename=file.filename, error=str(ve))
|
|
166
|
+
except Exception as e:
|
|
167
|
+
results.append({
|
|
168
|
+
"filename": file.filename or "unknown",
|
|
169
|
+
"status": "error",
|
|
170
|
+
"error": str(e),
|
|
171
|
+
})
|
|
172
|
+
logger.error("file_upload_error", filename=file.filename, error=str(e))
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"status": "success" if total_added > 0 else "error",
|
|
176
|
+
"added": total_added,
|
|
177
|
+
"ids": all_ids,
|
|
178
|
+
"total_vectors": rag_service.get_stats()["total_vectors"],
|
|
179
|
+
"files": results,
|
|
180
|
+
}
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.error("upload_files_error", error=str(e))
|
|
183
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@router.get("/rag/formats")
|
|
187
|
+
async def supported_formats():
|
|
188
|
+
"""List supported file formats for upload."""
|
|
189
|
+
return get_supported_formats()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@router.post("/rag/documents")
|
|
193
|
+
async def add_documents(request: AddDocumentsRequest):
|
|
194
|
+
"""Add documents to the vector store (JSON/text — for programmatic use)."""
|
|
195
|
+
try:
|
|
196
|
+
result = await rag_service.add_documents(request.documents, request.metadatas)
|
|
197
|
+
return result
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error("add_documents_error", error=str(e))
|
|
200
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@router.post("/rag/search", response_model=SearchResponse)
|
|
204
|
+
async def search_documents(request: SearchRequest):
|
|
205
|
+
"""Similarity search in vector store."""
|
|
206
|
+
try:
|
|
207
|
+
results = await rag_service.search_documents(request.query, request.k)
|
|
208
|
+
return {"results": results, "query": request.query}
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error("search_documents_error", error=str(e))
|
|
211
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@router.post("/rag/chat")
|
|
215
|
+
async def rag_chat(request: RAGChatRequest):
|
|
216
|
+
"""RAG-augmented chat completion."""
|
|
217
|
+
try:
|
|
218
|
+
response = await rag_service.rag_chat(
|
|
219
|
+
query=request.query,
|
|
220
|
+
model=request.model,
|
|
221
|
+
k=request.k,
|
|
222
|
+
temperature=request.temperature,
|
|
223
|
+
max_tokens=request.max_tokens,
|
|
224
|
+
provider_id=request.provider_id,
|
|
225
|
+
user_id=request.user_id,
|
|
226
|
+
)
|
|
227
|
+
return response
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error("rag_chat_error", error=str(e))
|
|
230
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@router.get("/rag/stats", response_model=VectorStoreStats)
|
|
234
|
+
async def rag_stats():
|
|
235
|
+
"""Vector store statistics."""
|
|
236
|
+
try:
|
|
237
|
+
return rag_service.get_stats()
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.error("get_stats_error", error=str(e))
|
|
240
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@router.delete("/rag/clear")
|
|
244
|
+
async def clear_rag():
|
|
245
|
+
"""Clear all documents from vector store."""
|
|
246
|
+
try:
|
|
247
|
+
return rag_service.clear_store()
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.error("clear_store_error", error=str(e))
|
|
250
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@router.delete("/rag/delete", response_model=DeleteDocumentsResponse)
|
|
254
|
+
async def delete_documents(request: DeleteDocumentsRequest):
|
|
255
|
+
"""Delete specific documents from vector store by ID."""
|
|
256
|
+
try:
|
|
257
|
+
result = await rag_service.delete_documents(request.document_ids)
|
|
258
|
+
return result
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.error("delete_documents_error", error=str(e))
|
|
261
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@router.post("/embeddings", response_model=EmbeddingResponse)
|
|
265
|
+
async def create_embeddings(request: EmbeddingRequest):
|
|
266
|
+
"""Generate embeddings."""
|
|
267
|
+
try:
|
|
268
|
+
return await embedding_service.create_embeddings(
|
|
269
|
+
request,
|
|
270
|
+
provider_id=request.provider_id,
|
|
271
|
+
user_id=request.user_id,
|
|
272
|
+
)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.error("embeddings_error", error=str(e))
|
|
275
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
// ============================================================================
|
|
279
|
+
// Guardrails Routes — /api/guardrails/*
|
|
280
|
+
// ============================================================================
|
|
281
|
+
function generateGuardrailsRoutes() {
|
|
282
|
+
return `"""Guardrails API Routes — Auto-generated by ChimerAI CLI."""
|
|
283
|
+
|
|
284
|
+
from fastapi import APIRouter, HTTPException
|
|
285
|
+
from pydantic import BaseModel
|
|
286
|
+
import structlog
|
|
287
|
+
|
|
288
|
+
from services.guardrails_service import guardrails_service
|
|
289
|
+
|
|
290
|
+
logger = structlog.get_logger()
|
|
291
|
+
|
|
292
|
+
router = APIRouter(prefix="/api/guardrails", tags=["guardrails"])
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class GuardrailsRequest(BaseModel):
|
|
296
|
+
text: str
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@router.post("/pii/detect")
|
|
300
|
+
async def detect_pii(request: GuardrailsRequest):
|
|
301
|
+
"""Detect PII in text."""
|
|
302
|
+
try:
|
|
303
|
+
return await guardrails_service.detect_pii(request.text)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.error("pii_detect_error", error=str(e))
|
|
306
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@router.post("/pii/redact")
|
|
310
|
+
async def redact_pii(request: GuardrailsRequest):
|
|
311
|
+
"""Redact PII from text."""
|
|
312
|
+
try:
|
|
313
|
+
return await guardrails_service.redact_pii(request.text)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error("pii_redact_error", error=str(e))
|
|
316
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@router.post("/toxicity")
|
|
320
|
+
async def check_toxicity(request: GuardrailsRequest):
|
|
321
|
+
"""Check text for toxic content."""
|
|
322
|
+
try:
|
|
323
|
+
return await guardrails_service.check_toxicity(request.text)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.error("toxicity_check_error", error=str(e))
|
|
326
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@router.post("/injection")
|
|
330
|
+
async def detect_injection(request: GuardrailsRequest):
|
|
331
|
+
"""Detect prompt injection attempts."""
|
|
332
|
+
try:
|
|
333
|
+
return await guardrails_service.detect_prompt_injection(request.text)
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.error("injection_detect_error", error=str(e))
|
|
336
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
// ============================================================================
|
|
340
|
+
// Tools Routes — /api/tools/* (dynamically generated based on installed tools)
|
|
341
|
+
// ============================================================================
|
|
342
|
+
/**
|
|
343
|
+
* Per-tool route blocks. Each block contains imports, request models, and endpoints.
|
|
344
|
+
* Only blocks for installed tools are included in the generated tools_routes.py.
|
|
345
|
+
*/
|
|
346
|
+
const TOOL_ROUTE_BLOCKS = {
|
|
347
|
+
web_tools: `
|
|
348
|
+
# --- Web Search & Scraping ---
|
|
349
|
+
from services.tools.web_tools import web_search, web_scraper
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class WebSearchRequest(BaseModel):
|
|
353
|
+
query: str
|
|
354
|
+
max_results: int = 10
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class WebScrapeRequest(BaseModel):
|
|
358
|
+
url: str
|
|
359
|
+
format: str = "markdown"
|
|
360
|
+
render_js: bool = False
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@router.post("/web/search")
|
|
364
|
+
async def tool_web_search(request: WebSearchRequest):
|
|
365
|
+
"""Search the web using DuckDuckGo."""
|
|
366
|
+
try:
|
|
367
|
+
results = await web_search.search(request.query, request.max_results)
|
|
368
|
+
return {"results": results}
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logger.error("web_search_error", error=str(e))
|
|
371
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@router.post("/web/scrape")
|
|
375
|
+
async def tool_web_scrape(request: WebScrapeRequest):
|
|
376
|
+
"""Scrape content from URL."""
|
|
377
|
+
try:
|
|
378
|
+
result = await web_scraper.scrape_url(
|
|
379
|
+
request.url,
|
|
380
|
+
format=request.format,
|
|
381
|
+
render_js=request.render_js,
|
|
382
|
+
)
|
|
383
|
+
return result
|
|
384
|
+
except Exception as e:
|
|
385
|
+
logger.error("web_scrape_error", error=str(e))
|
|
386
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
387
|
+
`,
|
|
388
|
+
document_tools: `
|
|
389
|
+
# --- Document Processing ---
|
|
390
|
+
from services.tools.document_tools import pdf_tool, document_tool
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@router.post("/document/process")
|
|
394
|
+
async def tool_document_process(file: UploadFile = File(...)):
|
|
395
|
+
"""Process uploaded document (PDF, DOCX, XLSX, PPTX, TXT)."""
|
|
396
|
+
try:
|
|
397
|
+
import tempfile
|
|
398
|
+
import os
|
|
399
|
+
|
|
400
|
+
with tempfile.NamedTemporaryFile(
|
|
401
|
+
delete=False, suffix=os.path.splitext(file.filename)[1]
|
|
402
|
+
) as tmp:
|
|
403
|
+
content = await file.read()
|
|
404
|
+
tmp.write(content)
|
|
405
|
+
tmp_path = tmp.name
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
result = await document_tool.process_file(tmp_path)
|
|
409
|
+
return result
|
|
410
|
+
finally:
|
|
411
|
+
os.unlink(tmp_path)
|
|
412
|
+
except Exception as e:
|
|
413
|
+
logger.error("document_process_error", error=str(e))
|
|
414
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
415
|
+
`,
|
|
416
|
+
code_tools: `
|
|
417
|
+
# --- Code Execution ---
|
|
418
|
+
from services.tools.code_tools import code_interpreter
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class CodeExecuteRequest(BaseModel):
|
|
422
|
+
code: str
|
|
423
|
+
inputs: Optional[dict] = None
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@router.post("/code/execute")
|
|
427
|
+
async def tool_code_execute(request: CodeExecuteRequest):
|
|
428
|
+
"""Execute Python code in sandbox."""
|
|
429
|
+
try:
|
|
430
|
+
result = await code_interpreter.execute_code(request.code, request.inputs)
|
|
431
|
+
return result
|
|
432
|
+
except Exception as e:
|
|
433
|
+
logger.error("code_execute_error", error=str(e))
|
|
434
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
435
|
+
`,
|
|
436
|
+
nlp_tools: `
|
|
437
|
+
# --- NLP Toolkit ---
|
|
438
|
+
from services.tools.nlp_tools import (
|
|
439
|
+
summarization_tool,
|
|
440
|
+
sentiment_tool,
|
|
441
|
+
information_extractor,
|
|
442
|
+
text_classifier,
|
|
443
|
+
text_chunker,
|
|
444
|
+
qa_chain,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class SummarizeRequest(BaseModel):
|
|
449
|
+
text: str
|
|
450
|
+
mode: str = "concise"
|
|
451
|
+
max_length: Optional[int] = None
|
|
452
|
+
language: str = "en"
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class SentimentRequest(BaseModel):
|
|
456
|
+
text: str
|
|
457
|
+
include_emotions: bool = True
|
|
458
|
+
include_aspects: bool = False
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class ExtractInfoRequest(BaseModel):
|
|
462
|
+
text: str
|
|
463
|
+
schema: Optional[dict] = None
|
|
464
|
+
entity_types: Optional[List[str]] = None
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class ClassifyRequest(BaseModel):
|
|
468
|
+
text: str
|
|
469
|
+
categories: List[str]
|
|
470
|
+
multi_label: bool = False
|
|
471
|
+
include_confidence: bool = True
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class ChunkRequest(BaseModel):
|
|
475
|
+
text: str
|
|
476
|
+
strategy: str = "token_based"
|
|
477
|
+
chunk_size: int = 1000
|
|
478
|
+
chunk_overlap: int = 200
|
|
479
|
+
max_chunks: Optional[int] = None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class QARequest(BaseModel):
|
|
483
|
+
question: str
|
|
484
|
+
context: Optional[str] = None
|
|
485
|
+
context_documents: Optional[List[str]] = None
|
|
486
|
+
include_sources: bool = True
|
|
487
|
+
include_confidence: bool = True
|
|
488
|
+
chain_of_thought: bool = False
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@router.post("/nlp/summarize")
|
|
492
|
+
async def summarize_text(request: SummarizeRequest):
|
|
493
|
+
"""Summarize text using different modes."""
|
|
494
|
+
try:
|
|
495
|
+
return await summarization_tool.summarize(
|
|
496
|
+
text=request.text,
|
|
497
|
+
mode=request.mode,
|
|
498
|
+
max_length=request.max_length,
|
|
499
|
+
language=request.language,
|
|
500
|
+
)
|
|
501
|
+
except Exception as e:
|
|
502
|
+
logger.error("summarization_error", error=str(e))
|
|
503
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@router.post("/nlp/sentiment")
|
|
507
|
+
async def analyze_sentiment(request: SentimentRequest):
|
|
508
|
+
"""Analyze sentiment and emotions in text."""
|
|
509
|
+
try:
|
|
510
|
+
return await sentiment_tool.analyze_sentiment(
|
|
511
|
+
text=request.text,
|
|
512
|
+
include_emotions=request.include_emotions,
|
|
513
|
+
include_aspects=request.include_aspects,
|
|
514
|
+
)
|
|
515
|
+
except Exception as e:
|
|
516
|
+
logger.error("sentiment_analysis_error", error=str(e))
|
|
517
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@router.post("/nlp/extract")
|
|
521
|
+
async def extract_information(request: ExtractInfoRequest):
|
|
522
|
+
"""Extract structured information from text."""
|
|
523
|
+
try:
|
|
524
|
+
return await information_extractor.extract_information(
|
|
525
|
+
text=request.text,
|
|
526
|
+
schema=request.schema,
|
|
527
|
+
entity_types=request.entity_types,
|
|
528
|
+
)
|
|
529
|
+
except Exception as e:
|
|
530
|
+
logger.error("information_extraction_error", error=str(e))
|
|
531
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@router.post("/nlp/classify")
|
|
535
|
+
async def classify_text(request: ClassifyRequest):
|
|
536
|
+
"""Classify text into categories."""
|
|
537
|
+
try:
|
|
538
|
+
return await text_classifier.classify(
|
|
539
|
+
text=request.text,
|
|
540
|
+
categories=request.categories,
|
|
541
|
+
multi_label=request.multi_label,
|
|
542
|
+
include_confidence=request.include_confidence,
|
|
543
|
+
)
|
|
544
|
+
except Exception as e:
|
|
545
|
+
logger.error("text_classification_error", error=str(e))
|
|
546
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@router.post("/nlp/chunk")
|
|
550
|
+
async def chunk_text(request: ChunkRequest):
|
|
551
|
+
"""Split text into chunks using various strategies."""
|
|
552
|
+
try:
|
|
553
|
+
return text_chunker.chunk_text(
|
|
554
|
+
text=request.text,
|
|
555
|
+
strategy=request.strategy,
|
|
556
|
+
chunk_size=request.chunk_size,
|
|
557
|
+
chunk_overlap=request.chunk_overlap,
|
|
558
|
+
max_chunks=request.max_chunks,
|
|
559
|
+
)
|
|
560
|
+
except Exception as e:
|
|
561
|
+
logger.error("text_chunking_error", error=str(e))
|
|
562
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@router.post("/nlp/qa")
|
|
566
|
+
async def answer_question(request: QARequest):
|
|
567
|
+
"""Answer questions with optional context."""
|
|
568
|
+
try:
|
|
569
|
+
return await qa_chain.answer_question(
|
|
570
|
+
question=request.question,
|
|
571
|
+
context=request.context,
|
|
572
|
+
context_documents=request.context_documents,
|
|
573
|
+
include_sources=request.include_sources,
|
|
574
|
+
include_confidence=request.include_confidence,
|
|
575
|
+
chain_of_thought=request.chain_of_thought,
|
|
576
|
+
)
|
|
577
|
+
except Exception as e:
|
|
578
|
+
logger.error("qa_chain_error", error=str(e))
|
|
579
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
580
|
+
`,
|
|
581
|
+
vision_tools: `
|
|
582
|
+
# --- Vision / Image Analysis ---
|
|
583
|
+
from services.tools.vision_tools import vision_tool
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class ImageAnalyzeRequest(BaseModel):
|
|
587
|
+
image_url: Optional[str] = None
|
|
588
|
+
image_path: Optional[str] = None
|
|
589
|
+
prompt: str = "Describe this image in detail."
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@router.post("/vision/analyze")
|
|
593
|
+
async def tool_vision_analyze(request: ImageAnalyzeRequest):
|
|
594
|
+
"""Analyze image with GPT-4 Vision."""
|
|
595
|
+
try:
|
|
596
|
+
result = await vision_tool.analyze_image(
|
|
597
|
+
image_url=request.image_url,
|
|
598
|
+
image_path=request.image_path,
|
|
599
|
+
prompt=request.prompt,
|
|
600
|
+
)
|
|
601
|
+
return result
|
|
602
|
+
except Exception as e:
|
|
603
|
+
logger.error("vision_analyze_error", error=str(e))
|
|
604
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
605
|
+
`,
|
|
606
|
+
google_sheets_tools: `
|
|
607
|
+
# --- Google Sheets ---
|
|
608
|
+
from services.tools.google_sheets_tools import google_sheets_tools
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
class ReadSheetRequest(BaseModel):
|
|
612
|
+
spreadsheet_id: str
|
|
613
|
+
range_name: str
|
|
614
|
+
value_render_option: str = "FORMATTED_VALUE"
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
class WriteSheetRequest(BaseModel):
|
|
618
|
+
spreadsheet_id: str
|
|
619
|
+
range_name: str
|
|
620
|
+
values: List[List]
|
|
621
|
+
value_input_option: str = "USER_ENTERED"
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
class AppendRowsRequest(BaseModel):
|
|
625
|
+
spreadsheet_id: str
|
|
626
|
+
range_name: str
|
|
627
|
+
values: List[List]
|
|
628
|
+
value_input_option: str = "USER_ENTERED"
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class UpdateCellRequest(BaseModel):
|
|
632
|
+
spreadsheet_id: str
|
|
633
|
+
cell: str
|
|
634
|
+
value: Any
|
|
635
|
+
value_input_option: str = "USER_ENTERED"
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
class CreateSheetRequest(BaseModel):
|
|
639
|
+
spreadsheet_id: str
|
|
640
|
+
sheet_title: str
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class ClearSheetRequest(BaseModel):
|
|
644
|
+
spreadsheet_id: str
|
|
645
|
+
range_name: str
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@router.post("/sheets/read")
|
|
649
|
+
async def read_sheet(request: ReadSheetRequest):
|
|
650
|
+
"""Read data from a Google Sheet."""
|
|
651
|
+
try:
|
|
652
|
+
return await google_sheets_tools.read_sheet(
|
|
653
|
+
spreadsheet_id=request.spreadsheet_id,
|
|
654
|
+
range_name=request.range_name,
|
|
655
|
+
value_render_option=request.value_render_option,
|
|
656
|
+
)
|
|
657
|
+
except Exception as e:
|
|
658
|
+
logger.error("read_sheet_error", error=str(e))
|
|
659
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
@router.post("/sheets/write")
|
|
663
|
+
async def write_sheet(request: WriteSheetRequest):
|
|
664
|
+
"""Write data to a Google Sheet."""
|
|
665
|
+
try:
|
|
666
|
+
return await google_sheets_tools.write_sheet(
|
|
667
|
+
spreadsheet_id=request.spreadsheet_id,
|
|
668
|
+
range_name=request.range_name,
|
|
669
|
+
values=request.values,
|
|
670
|
+
value_input_option=request.value_input_option,
|
|
671
|
+
)
|
|
672
|
+
except Exception as e:
|
|
673
|
+
logger.error("write_sheet_error", error=str(e))
|
|
674
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
@router.post("/sheets/append")
|
|
678
|
+
async def append_rows(request: AppendRowsRequest):
|
|
679
|
+
"""Append rows to a Google Sheet."""
|
|
680
|
+
try:
|
|
681
|
+
return await google_sheets_tools.append_rows(
|
|
682
|
+
spreadsheet_id=request.spreadsheet_id,
|
|
683
|
+
range_name=request.range_name,
|
|
684
|
+
values=request.values,
|
|
685
|
+
value_input_option=request.value_input_option,
|
|
686
|
+
)
|
|
687
|
+
except Exception as e:
|
|
688
|
+
logger.error("append_rows_error", error=str(e))
|
|
689
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
@router.post("/sheets/update-cell")
|
|
693
|
+
async def update_cell(request: UpdateCellRequest):
|
|
694
|
+
"""Update a single cell."""
|
|
695
|
+
try:
|
|
696
|
+
return await google_sheets_tools.update_cell(
|
|
697
|
+
spreadsheet_id=request.spreadsheet_id,
|
|
698
|
+
cell=request.cell,
|
|
699
|
+
value=request.value,
|
|
700
|
+
value_input_option=request.value_input_option,
|
|
701
|
+
)
|
|
702
|
+
except Exception as e:
|
|
703
|
+
logger.error("update_cell_error", error=str(e))
|
|
704
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
@router.post("/sheets/create-sheet")
|
|
708
|
+
async def create_sheet(request: CreateSheetRequest):
|
|
709
|
+
"""Create a new sheet in an existing spreadsheet."""
|
|
710
|
+
try:
|
|
711
|
+
return await google_sheets_tools.create_sheet(
|
|
712
|
+
spreadsheet_id=request.spreadsheet_id,
|
|
713
|
+
sheet_title=request.sheet_title,
|
|
714
|
+
)
|
|
715
|
+
except Exception as e:
|
|
716
|
+
logger.error("create_sheet_error", error=str(e))
|
|
717
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@router.post("/sheets/clear")
|
|
721
|
+
async def clear_sheet(request: ClearSheetRequest):
|
|
722
|
+
"""Clear data from a range."""
|
|
723
|
+
try:
|
|
724
|
+
return await google_sheets_tools.clear_sheet(
|
|
725
|
+
spreadsheet_id=request.spreadsheet_id,
|
|
726
|
+
range_name=request.range_name,
|
|
727
|
+
)
|
|
728
|
+
except Exception as e:
|
|
729
|
+
logger.error("clear_sheet_error", error=str(e))
|
|
730
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
731
|
+
`,
|
|
732
|
+
airtable_tools: `
|
|
733
|
+
# --- Airtable ---
|
|
734
|
+
from services.tools.airtable_tools import airtable_tools
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
class GetAirtableRecordsRequest(BaseModel):
|
|
738
|
+
base_id: str
|
|
739
|
+
table_name: str
|
|
740
|
+
max_records: Optional[int] = None
|
|
741
|
+
view: Optional[str] = None
|
|
742
|
+
formula: Optional[str] = None
|
|
743
|
+
sort: Optional[List[str]] = None
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
class GetAirtableRecordRequest(BaseModel):
|
|
747
|
+
base_id: str
|
|
748
|
+
table_name: str
|
|
749
|
+
record_id: str
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
class CreateAirtableRecordRequest(BaseModel):
|
|
753
|
+
base_id: str
|
|
754
|
+
table_name: str
|
|
755
|
+
fields: Dict[str, Any]
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
class CreateAirtableRecordsRequest(BaseModel):
|
|
759
|
+
base_id: str
|
|
760
|
+
table_name: str
|
|
761
|
+
records: List[Dict[str, Any]]
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class UpdateAirtableRecordRequest(BaseModel):
|
|
765
|
+
base_id: str
|
|
766
|
+
table_name: str
|
|
767
|
+
record_id: str
|
|
768
|
+
fields: Dict[str, Any]
|
|
769
|
+
replace: bool = False
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
class DeleteAirtableRecordRequest(BaseModel):
|
|
773
|
+
base_id: str
|
|
774
|
+
table_name: str
|
|
775
|
+
record_id: str
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
class SearchAirtableRequest(BaseModel):
|
|
779
|
+
base_id: str
|
|
780
|
+
table_name: str
|
|
781
|
+
field_name: str
|
|
782
|
+
field_value: Any
|
|
783
|
+
max_records: Optional[int] = None
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
@router.post("/airtable/get-records")
|
|
787
|
+
async def get_airtable_records(request: GetAirtableRecordsRequest):
|
|
788
|
+
"""Get records from an Airtable table."""
|
|
789
|
+
try:
|
|
790
|
+
return await airtable_tools.get_records(
|
|
791
|
+
base_id=request.base_id,
|
|
792
|
+
table_name=request.table_name,
|
|
793
|
+
max_records=request.max_records,
|
|
794
|
+
view=request.view,
|
|
795
|
+
formula=request.formula,
|
|
796
|
+
sort=request.sort,
|
|
797
|
+
)
|
|
798
|
+
except Exception as e:
|
|
799
|
+
logger.error("get_airtable_records_error", error=str(e))
|
|
800
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
@router.post("/airtable/get-record")
|
|
804
|
+
async def get_airtable_record(request: GetAirtableRecordRequest):
|
|
805
|
+
"""Get a single record by ID."""
|
|
806
|
+
try:
|
|
807
|
+
return await airtable_tools.get_record(
|
|
808
|
+
base_id=request.base_id,
|
|
809
|
+
table_name=request.table_name,
|
|
810
|
+
record_id=request.record_id,
|
|
811
|
+
)
|
|
812
|
+
except Exception as e:
|
|
813
|
+
logger.error("get_airtable_record_error", error=str(e))
|
|
814
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
@router.post("/airtable/create-record")
|
|
818
|
+
async def create_airtable_record(request: CreateAirtableRecordRequest):
|
|
819
|
+
"""Create a new record."""
|
|
820
|
+
try:
|
|
821
|
+
return await airtable_tools.create_record(
|
|
822
|
+
base_id=request.base_id,
|
|
823
|
+
table_name=request.table_name,
|
|
824
|
+
fields=request.fields,
|
|
825
|
+
)
|
|
826
|
+
except Exception as e:
|
|
827
|
+
logger.error("create_airtable_record_error", error=str(e))
|
|
828
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
@router.post("/airtable/create-records")
|
|
832
|
+
async def create_airtable_records(request: CreateAirtableRecordsRequest):
|
|
833
|
+
"""Create multiple records (batch)."""
|
|
834
|
+
try:
|
|
835
|
+
return await airtable_tools.create_records(
|
|
836
|
+
base_id=request.base_id,
|
|
837
|
+
table_name=request.table_name,
|
|
838
|
+
records=request.records,
|
|
839
|
+
)
|
|
840
|
+
except Exception as e:
|
|
841
|
+
logger.error("create_airtable_records_error", error=str(e))
|
|
842
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
@router.post("/airtable/update-record")
|
|
846
|
+
async def update_airtable_record(request: UpdateAirtableRecordRequest):
|
|
847
|
+
"""Update an existing record."""
|
|
848
|
+
try:
|
|
849
|
+
return await airtable_tools.update_record(
|
|
850
|
+
base_id=request.base_id,
|
|
851
|
+
table_name=request.table_name,
|
|
852
|
+
record_id=request.record_id,
|
|
853
|
+
fields=request.fields,
|
|
854
|
+
replace=request.replace,
|
|
855
|
+
)
|
|
856
|
+
except Exception as e:
|
|
857
|
+
logger.error("update_airtable_record_error", error=str(e))
|
|
858
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
@router.post("/airtable/delete-record")
|
|
862
|
+
async def delete_airtable_record(request: DeleteAirtableRecordRequest):
|
|
863
|
+
"""Delete a record."""
|
|
864
|
+
try:
|
|
865
|
+
return await airtable_tools.delete_record(
|
|
866
|
+
base_id=request.base_id,
|
|
867
|
+
table_name=request.table_name,
|
|
868
|
+
record_id=request.record_id,
|
|
869
|
+
)
|
|
870
|
+
except Exception as e:
|
|
871
|
+
logger.error("delete_airtable_record_error", error=str(e))
|
|
872
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
@router.post("/airtable/search")
|
|
876
|
+
async def search_airtable_records(request: SearchAirtableRequest):
|
|
877
|
+
"""Search for records matching a field value."""
|
|
878
|
+
try:
|
|
879
|
+
return await airtable_tools.search_records(
|
|
880
|
+
base_id=request.base_id,
|
|
881
|
+
table_name=request.table_name,
|
|
882
|
+
field_name=request.field_name,
|
|
883
|
+
field_value=request.field_value,
|
|
884
|
+
max_records=request.max_records,
|
|
885
|
+
)
|
|
886
|
+
except Exception as e:
|
|
887
|
+
logger.error("search_airtable_error", error=str(e))
|
|
888
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
889
|
+
`,
|
|
890
|
+
deepl_tools: `
|
|
891
|
+
# --- DeepL Translation ---
|
|
892
|
+
from services.tools.deepl_tools import deepl_tools
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
class TranslateTextRequest(BaseModel):
|
|
896
|
+
text: Any # str or List[str]
|
|
897
|
+
target_lang: str
|
|
898
|
+
source_lang: Optional[str] = None
|
|
899
|
+
formality: Optional[str] = None
|
|
900
|
+
split_sentences: Optional[str] = None
|
|
901
|
+
preserve_formatting: bool = True
|
|
902
|
+
tag_handling: Optional[str] = None
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
class DetectLanguageRequest(BaseModel):
|
|
906
|
+
text: str
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
class GetSupportedLanguagesRequest(BaseModel):
|
|
910
|
+
language_type: str = "target"
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
class TranslateDocumentRequest(BaseModel):
|
|
914
|
+
input_path: str
|
|
915
|
+
output_path: str
|
|
916
|
+
target_lang: str
|
|
917
|
+
source_lang: Optional[str] = None
|
|
918
|
+
formality: Optional[str] = None
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
@router.post("/translate/text")
|
|
922
|
+
async def translate_text(request: TranslateTextRequest):
|
|
923
|
+
"""Translate text using DeepL."""
|
|
924
|
+
try:
|
|
925
|
+
return await deepl_tools.translate_text(
|
|
926
|
+
text=request.text,
|
|
927
|
+
target_lang=request.target_lang,
|
|
928
|
+
source_lang=request.source_lang,
|
|
929
|
+
formality=request.formality,
|
|
930
|
+
split_sentences=request.split_sentences,
|
|
931
|
+
preserve_formatting=request.preserve_formatting,
|
|
932
|
+
tag_handling=request.tag_handling,
|
|
933
|
+
)
|
|
934
|
+
except Exception as e:
|
|
935
|
+
logger.error("translate_text_error", error=str(e))
|
|
936
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
@router.post("/translate/detect-language")
|
|
940
|
+
async def detect_language(request: DetectLanguageRequest):
|
|
941
|
+
"""Detect the language of text."""
|
|
942
|
+
try:
|
|
943
|
+
return await deepl_tools.detect_language(text=request.text)
|
|
944
|
+
except Exception as e:
|
|
945
|
+
logger.error("detect_language_error", error=str(e))
|
|
946
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
@router.get("/translate/usage")
|
|
950
|
+
async def get_translation_usage():
|
|
951
|
+
"""Get DeepL API usage statistics."""
|
|
952
|
+
try:
|
|
953
|
+
return await deepl_tools.get_usage()
|
|
954
|
+
except Exception as e:
|
|
955
|
+
logger.error("get_usage_error", error=str(e))
|
|
956
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
@router.post("/translate/supported-languages")
|
|
960
|
+
async def get_supported_languages(request: GetSupportedLanguagesRequest):
|
|
961
|
+
"""Get list of supported languages."""
|
|
962
|
+
try:
|
|
963
|
+
return await deepl_tools.get_supported_languages(
|
|
964
|
+
language_type=request.language_type
|
|
965
|
+
)
|
|
966
|
+
except Exception as e:
|
|
967
|
+
logger.error("get_languages_error", error=str(e))
|
|
968
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
@router.post("/translate/document")
|
|
972
|
+
async def translate_document(request: TranslateDocumentRequest):
|
|
973
|
+
"""Translate a document file."""
|
|
974
|
+
try:
|
|
975
|
+
return await deepl_tools.translate_document(
|
|
976
|
+
input_path=request.input_path,
|
|
977
|
+
output_path=request.output_path,
|
|
978
|
+
target_lang=request.target_lang,
|
|
979
|
+
source_lang=request.source_lang,
|
|
980
|
+
formality=request.formality,
|
|
981
|
+
)
|
|
982
|
+
except Exception as e:
|
|
983
|
+
logger.error("translate_document_error", error=str(e))
|
|
984
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
985
|
+
`,
|
|
986
|
+
webhook_tools: `
|
|
987
|
+
# --- Webhooks (n8n, Zapier, Make, Slack) ---
|
|
988
|
+
from services.tools.webhook_tools import webhook_tools
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
class CallWebhookRequest(BaseModel):
|
|
992
|
+
url: str
|
|
993
|
+
payload: Optional[Dict[str, Any]] = None
|
|
994
|
+
method: str = "POST"
|
|
995
|
+
headers: Optional[Dict[str, str]] = None
|
|
996
|
+
query_params: Optional[Dict[str, str]] = None
|
|
997
|
+
auth_token: Optional[str] = None
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
class CallN8nWebhookRequest(BaseModel):
|
|
1001
|
+
webhook_id: str
|
|
1002
|
+
payload: Dict[str, Any]
|
|
1003
|
+
n8n_url: Optional[str] = None
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
class CallZapierWebhookRequest(BaseModel):
|
|
1007
|
+
hook_id: str
|
|
1008
|
+
payload: Dict[str, Any]
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
class CallMakeWebhookRequest(BaseModel):
|
|
1012
|
+
webhook_path: str
|
|
1013
|
+
payload: Dict[str, Any]
|
|
1014
|
+
region: str = "eu1"
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
class NotifySlackRequest(BaseModel):
|
|
1018
|
+
webhook_url: str
|
|
1019
|
+
message: str
|
|
1020
|
+
channel: Optional[str] = None
|
|
1021
|
+
username: Optional[str] = None
|
|
1022
|
+
icon_emoji: Optional[str] = None
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
@router.post("/webhook/call")
|
|
1026
|
+
async def call_webhook(request: CallWebhookRequest):
|
|
1027
|
+
"""Call any webhook endpoint."""
|
|
1028
|
+
try:
|
|
1029
|
+
return await webhook_tools.call_webhook(
|
|
1030
|
+
url=request.url,
|
|
1031
|
+
payload=request.payload,
|
|
1032
|
+
method=request.method,
|
|
1033
|
+
headers=request.headers,
|
|
1034
|
+
query_params=request.query_params,
|
|
1035
|
+
auth_token=request.auth_token,
|
|
1036
|
+
)
|
|
1037
|
+
except Exception as e:
|
|
1038
|
+
logger.error("webhook_call_error", error=str(e))
|
|
1039
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
@router.post("/webhook/n8n")
|
|
1043
|
+
async def call_n8n_webhook(request: CallN8nWebhookRequest):
|
|
1044
|
+
"""Call an n8n webhook."""
|
|
1045
|
+
try:
|
|
1046
|
+
return await webhook_tools.call_n8n_webhook(
|
|
1047
|
+
webhook_id=request.webhook_id,
|
|
1048
|
+
payload=request.payload,
|
|
1049
|
+
n8n_url=request.n8n_url,
|
|
1050
|
+
)
|
|
1051
|
+
except Exception as e:
|
|
1052
|
+
logger.error("n8n_webhook_error", error=str(e))
|
|
1053
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
@router.post("/webhook/zapier")
|
|
1057
|
+
async def call_zapier_webhook(request: CallZapierWebhookRequest):
|
|
1058
|
+
"""Call a Zapier webhook."""
|
|
1059
|
+
try:
|
|
1060
|
+
return await webhook_tools.call_zapier_webhook(
|
|
1061
|
+
hook_id=request.hook_id,
|
|
1062
|
+
payload=request.payload,
|
|
1063
|
+
)
|
|
1064
|
+
except Exception as e:
|
|
1065
|
+
logger.error("zapier_webhook_error", error=str(e))
|
|
1066
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
@router.post("/webhook/make")
|
|
1070
|
+
async def call_make_webhook(request: CallMakeWebhookRequest):
|
|
1071
|
+
"""Call a Make (Integromat) webhook."""
|
|
1072
|
+
try:
|
|
1073
|
+
return await webhook_tools.call_make_webhook(
|
|
1074
|
+
webhook_path=request.webhook_path,
|
|
1075
|
+
payload=request.payload,
|
|
1076
|
+
region=request.region,
|
|
1077
|
+
)
|
|
1078
|
+
except Exception as e:
|
|
1079
|
+
logger.error("make_webhook_error", error=str(e))
|
|
1080
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
@router.post("/webhook/slack")
|
|
1084
|
+
async def notify_slack(request: NotifySlackRequest):
|
|
1085
|
+
"""Send Slack notification via incoming webhook."""
|
|
1086
|
+
try:
|
|
1087
|
+
return await webhook_tools.notify_slack_via_webhook(
|
|
1088
|
+
webhook_url=request.webhook_url,
|
|
1089
|
+
message=request.message,
|
|
1090
|
+
channel=request.channel,
|
|
1091
|
+
username=request.username,
|
|
1092
|
+
icon_emoji=request.icon_emoji,
|
|
1093
|
+
)
|
|
1094
|
+
except Exception as e:
|
|
1095
|
+
logger.error("slack_notify_error", error=str(e))
|
|
1096
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1097
|
+
`,
|
|
1098
|
+
};
|
|
1099
|
+
/**
|
|
1100
|
+
* Generate tools_routes.py based on installed tools.
|
|
1101
|
+
* Only includes route blocks for the specified tools.
|
|
1102
|
+
*/
|
|
1103
|
+
function generateToolsRoutes(tools) {
|
|
1104
|
+
const blocks = tools.map((t) => TOOL_ROUTE_BLOCKS[t]).filter(Boolean);
|
|
1105
|
+
// Determine which extra imports are needed
|
|
1106
|
+
const needsUploadFile = tools.includes('document_tools');
|
|
1107
|
+
let extraImports = '';
|
|
1108
|
+
if (needsUploadFile) {
|
|
1109
|
+
extraImports = '\nfrom fastapi import UploadFile, File';
|
|
1110
|
+
}
|
|
1111
|
+
return `"""AI Tools Routes — Auto-generated by ChimerAI CLI."""
|
|
1112
|
+
|
|
1113
|
+
from fastapi import APIRouter, HTTPException${extraImports}
|
|
1114
|
+
from pydantic import BaseModel
|
|
1115
|
+
from typing import Optional, List, Dict, Any
|
|
1116
|
+
import structlog
|
|
1117
|
+
|
|
1118
|
+
logger = structlog.get_logger()
|
|
1119
|
+
|
|
1120
|
+
router = APIRouter(prefix="/api/tools", tags=["tools"])
|
|
1121
|
+
${blocks.join('\n')}
|
|
1122
|
+
`;
|
|
1123
|
+
}
|
|
1124
|
+
// ============================================================================
|
|
1125
|
+
// Routes __init__.py
|
|
1126
|
+
// ============================================================================
|
|
1127
|
+
function generateRoutesInit() {
|
|
1128
|
+
return `# Routes package — Auto-generated by ChimerAI CLI
|
|
1129
|
+
`;
|
|
1130
|
+
}
|