@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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +317 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.d.ts.map +1 -0
  8. package/dist/commands/add.js +2126 -0
  9. package/dist/commands/create.d.ts +12 -0
  10. package/dist/commands/create.d.ts.map +1 -0
  11. package/dist/commands/create.js +1703 -0
  12. package/dist/commands/deploy.d.ts +11 -0
  13. package/dist/commands/deploy.d.ts.map +1 -0
  14. package/dist/commands/deploy.js +219 -0
  15. package/dist/commands/dev.d.ts +17 -0
  16. package/dist/commands/dev.d.ts.map +1 -0
  17. package/dist/commands/dev.js +206 -0
  18. package/dist/commands/doctor.d.ts +11 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +728 -0
  21. package/dist/commands/generate.d.ts +19 -0
  22. package/dist/commands/generate.d.ts.map +1 -0
  23. package/dist/commands/generate.js +429 -0
  24. package/dist/commands/init.d.ts +11 -0
  25. package/dist/commands/init.d.ts.map +1 -0
  26. package/dist/commands/init.js +269 -0
  27. package/dist/commands/list.d.ts +12 -0
  28. package/dist/commands/list.d.ts.map +1 -0
  29. package/dist/commands/list.js +328 -0
  30. package/dist/commands/migrate.d.ts +14 -0
  31. package/dist/commands/migrate.d.ts.map +1 -0
  32. package/dist/commands/migrate.js +197 -0
  33. package/dist/commands/plugin.d.ts +10 -0
  34. package/dist/commands/plugin.d.ts.map +1 -0
  35. package/dist/commands/plugin.js +239 -0
  36. package/dist/commands/remove.d.ts +11 -0
  37. package/dist/commands/remove.d.ts.map +1 -0
  38. package/dist/commands/remove.js +472 -0
  39. package/dist/commands/secret.d.ts +12 -0
  40. package/dist/commands/secret.d.ts.map +1 -0
  41. package/dist/commands/secret.js +102 -0
  42. package/dist/commands/setup.d.ts +9 -0
  43. package/dist/commands/setup.d.ts.map +1 -0
  44. package/dist/commands/setup.js +788 -0
  45. package/dist/commands/update.d.ts +14 -0
  46. package/dist/commands/update.d.ts.map +1 -0
  47. package/dist/commands/update.js +211 -0
  48. package/dist/commands/use.d.ts +9 -0
  49. package/dist/commands/use.d.ts.map +1 -0
  50. package/dist/commands/use.js +51 -0
  51. package/dist/index.d.ts +22 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +45 -0
  54. package/dist/license.d.ts +55 -0
  55. package/dist/license.d.ts.map +1 -0
  56. package/dist/license.js +258 -0
  57. package/dist/scanner.d.ts +31 -0
  58. package/dist/scanner.d.ts.map +1 -0
  59. package/dist/scanner.js +113 -0
  60. package/dist/schema-manager.d.ts +26 -0
  61. package/dist/schema-manager.d.ts.map +1 -0
  62. package/dist/schema-manager.js +132 -0
  63. package/dist/templates/admin.d.ts +49 -0
  64. package/dist/templates/admin.d.ts.map +1 -0
  65. package/dist/templates/admin.js +1358 -0
  66. package/dist/templates/ai-routes.d.ts +17 -0
  67. package/dist/templates/ai-routes.d.ts.map +1 -0
  68. package/dist/templates/ai-routes.js +1130 -0
  69. package/dist/templates/ai-service-tools.d.ts +22 -0
  70. package/dist/templates/ai-service-tools.d.ts.map +1 -0
  71. package/dist/templates/ai-service-tools.js +1424 -0
  72. package/dist/templates/ai-service.d.ts +66 -0
  73. package/dist/templates/ai-service.d.ts.map +1 -0
  74. package/dist/templates/ai-service.js +2202 -0
  75. package/dist/templates/api-routes.d.ts +108 -0
  76. package/dist/templates/api-routes.d.ts.map +1 -0
  77. package/dist/templates/api-routes.js +1219 -0
  78. package/dist/templates/auth.d.ts +48 -0
  79. package/dist/templates/auth.d.ts.map +1 -0
  80. package/dist/templates/auth.js +381 -0
  81. package/dist/templates/billing.d.ts +44 -0
  82. package/dist/templates/billing.d.ts.map +1 -0
  83. package/dist/templates/billing.js +551 -0
  84. package/dist/templates/chat.d.ts +63 -0
  85. package/dist/templates/chat.d.ts.map +1 -0
  86. package/dist/templates/chat.js +1979 -0
  87. package/dist/templates/components.d.ts +22 -0
  88. package/dist/templates/components.d.ts.map +1 -0
  89. package/dist/templates/components.js +672 -0
  90. package/dist/templates/config.d.ts +6 -0
  91. package/dist/templates/config.d.ts.map +1 -0
  92. package/dist/templates/config.js +86 -0
  93. package/dist/templates/docker.d.ts +25 -0
  94. package/dist/templates/docker.d.ts.map +1 -0
  95. package/dist/templates/docker.js +165 -0
  96. package/dist/templates/gdpr.d.ts +16 -0
  97. package/dist/templates/gdpr.d.ts.map +1 -0
  98. package/dist/templates/gdpr.js +259 -0
  99. package/dist/templates/index.d.ts +77 -0
  100. package/dist/templates/index.d.ts.map +1 -0
  101. package/dist/templates/index.js +339 -0
  102. package/dist/templates/layout.d.ts +67 -0
  103. package/dist/templates/layout.d.ts.map +1 -0
  104. package/dist/templates/layout.js +670 -0
  105. package/dist/templates/mfa.d.ts +23 -0
  106. package/dist/templates/mfa.d.ts.map +1 -0
  107. package/dist/templates/mfa.js +353 -0
  108. package/dist/templates/middleware.d.ts +12 -0
  109. package/dist/templates/middleware.d.ts.map +1 -0
  110. package/dist/templates/middleware.js +116 -0
  111. package/dist/templates/prisma.d.ts +35 -0
  112. package/dist/templates/prisma.d.ts.map +1 -0
  113. package/dist/templates/prisma.js +724 -0
  114. package/dist/templates/provider-routes.d.ts +21 -0
  115. package/dist/templates/provider-routes.d.ts.map +1 -0
  116. package/dist/templates/provider-routes.js +1203 -0
  117. package/dist/templates/rag.d.ts +48 -0
  118. package/dist/templates/rag.d.ts.map +1 -0
  119. package/dist/templates/rag.js +532 -0
  120. package/dist/templates/widget.d.ts +64 -0
  121. package/dist/templates/widget.d.ts.map +1 -0
  122. package/dist/templates/widget.js +1360 -0
  123. package/dist/utils/provider-db.d.ts +63 -0
  124. package/dist/utils/provider-db.d.ts.map +1 -0
  125. package/dist/utils/provider-db.js +300 -0
  126. package/dist/utils.d.ts +78 -0
  127. package/dist/utils.d.ts.map +1 -0
  128. package/dist/utils.js +330 -0
  129. 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
+ }