kailash 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

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 (48) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/api/__init__.py +17 -0
  3. kailash/api/gateway.py +394 -0
  4. kailash/api/mcp_integration.py +478 -0
  5. kailash/api/workflow_api.py +399 -0
  6. kailash/nodes/ai/__init__.py +4 -4
  7. kailash/nodes/ai/agents.py +4 -4
  8. kailash/nodes/ai/ai_providers.py +18 -22
  9. kailash/nodes/ai/embedding_generator.py +34 -38
  10. kailash/nodes/ai/llm_agent.py +351 -356
  11. kailash/nodes/api/http.py +0 -4
  12. kailash/nodes/api/rest.py +1 -1
  13. kailash/nodes/base.py +60 -64
  14. kailash/nodes/code/python.py +61 -42
  15. kailash/nodes/data/__init__.py +10 -10
  16. kailash/nodes/data/readers.py +27 -29
  17. kailash/nodes/data/retrieval.py +1 -1
  18. kailash/nodes/data/sharepoint_graph.py +23 -25
  19. kailash/nodes/data/sql.py +27 -29
  20. kailash/nodes/data/vector_db.py +2 -2
  21. kailash/nodes/data/writers.py +41 -44
  22. kailash/nodes/logic/__init__.py +10 -3
  23. kailash/nodes/logic/async_operations.py +14 -14
  24. kailash/nodes/logic/operations.py +18 -22
  25. kailash/nodes/logic/workflow.py +439 -0
  26. kailash/nodes/mcp/client.py +29 -33
  27. kailash/nodes/mcp/resource.py +1 -1
  28. kailash/nodes/mcp/server.py +10 -4
  29. kailash/nodes/transform/formatters.py +1 -1
  30. kailash/nodes/transform/processors.py +5 -3
  31. kailash/runtime/docker.py +2 -0
  32. kailash/tracking/metrics_collector.py +6 -7
  33. kailash/tracking/models.py +0 -20
  34. kailash/tracking/storage/database.py +4 -4
  35. kailash/tracking/storage/filesystem.py +0 -1
  36. kailash/utils/export.py +2 -2
  37. kailash/utils/templates.py +16 -16
  38. kailash/visualization/performance.py +7 -7
  39. kailash/visualization/reports.py +1 -1
  40. kailash/workflow/graph.py +4 -4
  41. kailash/workflow/mock_registry.py +1 -1
  42. {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/METADATA +198 -27
  43. kailash-0.1.4.dist-info/RECORD +85 -0
  44. kailash-0.1.2.dist-info/RECORD +0 -80
  45. {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/WHEEL +0 -0
  46. {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/entry_points.txt +0 -0
  47. {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/licenses/LICENSE +0 -0
  48. {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,399 @@
1
+ """
2
+ Lean API wrapper for Kailash workflows using FastAPI.
3
+
4
+ This module provides a general-purpose API wrapper that can expose any Kailash
5
+ workflow as a REST API with minimal configuration.
6
+ """
7
+
8
+ import asyncio
9
+ from contextlib import asynccontextmanager
10
+ from enum import Enum
11
+ from typing import Any, Dict, List, Optional, Union
12
+
13
+ import uvicorn
14
+ from fastapi import BackgroundTasks, FastAPI, HTTPException
15
+ from fastapi.responses import StreamingResponse
16
+ from pydantic import BaseModel, Field
17
+
18
+ from kailash.runtime.local import LocalRuntime
19
+ from kailash.workflow.builder import WorkflowBuilder
20
+ from kailash.workflow.graph import Workflow
21
+
22
+
23
+ class ExecutionMode(str, Enum):
24
+ """Execution modes for workflow API."""
25
+
26
+ SYNC = "sync"
27
+ ASYNC = "async"
28
+ STREAM = "stream"
29
+
30
+
31
+ class WorkflowRequest(BaseModel):
32
+ """Base request model for workflow execution."""
33
+
34
+ inputs: Dict[str, Any] = Field(..., description="Input data for workflow nodes")
35
+ config: Optional[Dict[str, Any]] = Field(
36
+ None, description="Node configuration overrides"
37
+ )
38
+ mode: ExecutionMode = Field(ExecutionMode.SYNC, description="Execution mode")
39
+
40
+
41
+ class WorkflowResponse(BaseModel):
42
+ """Base response model for workflow execution."""
43
+
44
+ outputs: Dict[str, Any] = Field(..., description="Output data from workflow nodes")
45
+ execution_time: float = Field(..., description="Execution time in seconds")
46
+ workflow_id: str = Field(..., description="Workflow identifier")
47
+ version: str = Field(..., description="Workflow version")
48
+
49
+
50
+ class WorkflowAPI:
51
+ """
52
+ Lean API wrapper for Kailash workflows.
53
+
54
+ This class provides a minimal, efficient way to expose any Kailash workflow
55
+ as a REST API with support for synchronous, asynchronous, and streaming execution.
56
+
57
+ Example:
58
+ >>> # For any workflow
59
+ >>> from my_workflows import rag_workflow
60
+ >>> api = WorkflowAPI(rag_workflow)
61
+ >>> api.run(port=8000)
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ workflow: Union[WorkflowBuilder, Workflow],
67
+ app_name: str = "Kailash Workflow API",
68
+ version: str = "1.0.0",
69
+ description: str = "API wrapper for Kailash workflow execution",
70
+ ):
71
+ """
72
+ Initialize the API wrapper.
73
+
74
+ Args:
75
+ workflow: The WorkflowBuilder or Workflow instance to expose
76
+ app_name: Name of the API application
77
+ version: API version
78
+ description: API description
79
+ """
80
+ if isinstance(workflow, WorkflowBuilder):
81
+ self.workflow = workflow
82
+ self.workflow_graph = workflow.build()
83
+ self.workflow_id = getattr(workflow, "workflow_id", "unnamed")
84
+ self.version = getattr(workflow, "version", "1.0.0")
85
+ else: # Workflow instance
86
+ self.workflow = workflow
87
+ self.workflow_graph = workflow
88
+ self.workflow_id = workflow.workflow_id
89
+ self.version = workflow.version
90
+
91
+ self.runtime = LocalRuntime()
92
+
93
+ # Create FastAPI app with lifespan management
94
+ self.app = FastAPI(
95
+ title=app_name,
96
+ version=version,
97
+ description=description,
98
+ lifespan=self._lifespan,
99
+ )
100
+
101
+ # Setup routes
102
+ self._setup_routes()
103
+
104
+ # Cache for async executions
105
+ self._execution_cache: Dict[str, Dict[str, Any]] = {}
106
+
107
+ @asynccontextmanager
108
+ async def _lifespan(self, app: FastAPI):
109
+ """Manage app lifecycle."""
110
+ # Startup
111
+ yield
112
+ # Shutdown - cleanup cache
113
+ self._execution_cache.clear()
114
+
115
+ def _setup_routes(self):
116
+ """Setup API routes dynamically based on workflow."""
117
+
118
+ # Main execution endpoint
119
+ @self.app.post("/execute")
120
+ async def execute_workflow(
121
+ request: WorkflowRequest, background_tasks: BackgroundTasks
122
+ ):
123
+ """Execute the workflow with provided inputs."""
124
+
125
+ if request.mode == ExecutionMode.SYNC:
126
+ return await self._execute_sync(request)
127
+ elif request.mode == ExecutionMode.ASYNC:
128
+ return await self._execute_async(request, background_tasks)
129
+ else: # STREAM
130
+ return StreamingResponse(
131
+ self._execute_stream(request), media_type="application/json"
132
+ )
133
+
134
+ # Status endpoint for async executions
135
+ @self.app.get("/status/{execution_id}")
136
+ async def get_execution_status(execution_id: str):
137
+ """Get status of async execution."""
138
+ if execution_id not in self._execution_cache:
139
+ raise HTTPException(status_code=404, detail="Execution not found")
140
+ return self._execution_cache[execution_id]
141
+
142
+ # Workflow metadata endpoint
143
+ @self.app.get("/workflow/info")
144
+ async def get_workflow_info():
145
+ """Get workflow metadata and structure."""
146
+ workflow = self.workflow_graph
147
+
148
+ # Get node information
149
+ nodes = []
150
+ for node_id, node_instance in workflow.nodes.items():
151
+ nodes.append({"id": node_id, "type": node_instance.node_type})
152
+
153
+ # Get edge information
154
+ edges = []
155
+ for conn in workflow.connections:
156
+ edges.append(
157
+ {
158
+ "source": conn.source_node,
159
+ "target": conn.target_node,
160
+ "source_output": conn.source_output,
161
+ "target_input": conn.target_input,
162
+ }
163
+ )
164
+
165
+ return {
166
+ "workflow_id": workflow.workflow_id,
167
+ "name": workflow.name,
168
+ "description": workflow.description,
169
+ "version": workflow.version,
170
+ "nodes": nodes,
171
+ "edges": edges,
172
+ "node_count": len(nodes),
173
+ "edge_count": len(edges),
174
+ }
175
+
176
+ # Health check
177
+ @self.app.get("/health")
178
+ async def health_check():
179
+ """Check API health."""
180
+ return {"status": "healthy", "workflow": self.workflow_id}
181
+
182
+ async def _execute_sync(self, request: WorkflowRequest) -> WorkflowResponse:
183
+ """Execute workflow synchronously."""
184
+ import time
185
+
186
+ start_time = time.time()
187
+
188
+ try:
189
+ # Apply configuration overrides if provided
190
+ if request.config:
191
+ for node_id, config in request.config.items():
192
+ # This would need workflow builder enhancement to support
193
+ # dynamic config updates
194
+ pass
195
+
196
+ # Execute workflow with inputs
197
+ results = await asyncio.to_thread(
198
+ self.runtime.execute, self.workflow_graph, parameters=request.inputs
199
+ )
200
+
201
+ # Handle tuple return from runtime
202
+ if isinstance(results, tuple):
203
+ results = results[0] if results else {}
204
+
205
+ execution_time = time.time() - start_time
206
+
207
+ return WorkflowResponse(
208
+ outputs=results,
209
+ execution_time=execution_time,
210
+ workflow_id=self.workflow_id,
211
+ version=self.version,
212
+ )
213
+
214
+ except Exception as e:
215
+ raise HTTPException(status_code=500, detail=str(e))
216
+
217
+ async def _execute_async(
218
+ self, request: WorkflowRequest, background_tasks: BackgroundTasks
219
+ ):
220
+ """Execute workflow asynchronously."""
221
+ import uuid
222
+
223
+ execution_id = str(uuid.uuid4())
224
+
225
+ # Initialize cache entry
226
+ self._execution_cache[execution_id] = {
227
+ "status": "pending",
228
+ "workflow_id": self.workflow_id,
229
+ "version": self.version,
230
+ }
231
+
232
+ # Schedule background execution
233
+ background_tasks.add_task(self._run_async_execution, execution_id, request)
234
+
235
+ return {
236
+ "execution_id": execution_id,
237
+ "status": "pending",
238
+ "message": f"Execution started. Check status at /status/{execution_id}",
239
+ }
240
+
241
+ async def _run_async_execution(self, execution_id: str, request: WorkflowRequest):
242
+ """Run async execution in background."""
243
+ try:
244
+ self._execution_cache[execution_id]["status"] = "running"
245
+
246
+ result = await self._execute_sync(request)
247
+
248
+ self._execution_cache[execution_id].update(
249
+ {"status": "completed", "result": result.dict()}
250
+ )
251
+
252
+ except Exception as e:
253
+ self._execution_cache[execution_id].update(
254
+ {"status": "failed", "error": str(e)}
255
+ )
256
+
257
+ async def _execute_stream(self, request: WorkflowRequest):
258
+ """Execute workflow with streaming response."""
259
+ import json
260
+ import time
261
+
262
+ try:
263
+ # For streaming, we'd need workflow runner enhancement
264
+ # to support progress callbacks. For now, simulate with
265
+ # start/end events
266
+
267
+ yield json.dumps(
268
+ {
269
+ "event": "start",
270
+ "workflow_id": self.workflow_id,
271
+ "timestamp": time.time(),
272
+ }
273
+ ) + "\n"
274
+
275
+ result = await self._execute_sync(request)
276
+
277
+ yield json.dumps(
278
+ {"event": "complete", "result": result.dict(), "timestamp": time.time()}
279
+ ) + "\n"
280
+
281
+ except Exception as e:
282
+ yield json.dumps(
283
+ {"event": "error", "error": str(e), "timestamp": time.time()}
284
+ ) + "\n"
285
+
286
+ def run(self, host: str = "0.0.0.0", port: int = 8000, **kwargs):
287
+ """Run the API server."""
288
+ uvicorn.run(self.app, host=host, port=port, **kwargs)
289
+
290
+
291
+ # Specialized API wrapper for Hierarchical RAG workflows
292
+ class HierarchicalRAGAPI(WorkflowAPI):
293
+ """
294
+ Specialized API wrapper for Hierarchical RAG workflows.
295
+
296
+ Provides RAG-specific endpoints and models for better developer experience.
297
+ """
298
+
299
+ def __init__(self, workflow: WorkflowBuilder, **kwargs):
300
+ super().__init__(workflow, **kwargs)
301
+ self._setup_rag_routes()
302
+
303
+ def _setup_rag_routes(self):
304
+ """Setup RAG-specific routes."""
305
+
306
+ class Document(BaseModel):
307
+ id: str
308
+ title: str
309
+ content: str
310
+
311
+ class RAGQuery(BaseModel):
312
+ query: str
313
+ top_k: int = 3
314
+ similarity_method: str = "cosine"
315
+ temperature: float = 0.7
316
+ max_tokens: int = 500
317
+
318
+ class RAGResponse(BaseModel):
319
+ answer: str
320
+ sources: List[Dict[str, Any]]
321
+ query: str
322
+ execution_time: float
323
+
324
+ @self.app.post("/documents")
325
+ async def add_documents(documents: List[Document]):
326
+ """Add documents to the knowledge base."""
327
+ # This would integrate with document storage
328
+ return {"message": f"Added {len(documents)} documents"}
329
+
330
+ @self.app.post("/query", response_model=RAGResponse)
331
+ async def query_rag(request: RAGQuery):
332
+ """Query the RAG system."""
333
+ import time
334
+
335
+ start_time = time.time()
336
+
337
+ # Transform to workflow format
338
+ workflow_request = WorkflowRequest(
339
+ inputs={
340
+ "query": request.query,
341
+ "config": {
342
+ "relevance_scorer": {
343
+ "top_k": request.top_k,
344
+ "similarity_method": request.similarity_method,
345
+ },
346
+ "llm_agent": {
347
+ "temperature": request.temperature,
348
+ "max_tokens": request.max_tokens,
349
+ },
350
+ },
351
+ }
352
+ )
353
+
354
+ result = await self._execute_sync(workflow_request)
355
+
356
+ # Extract RAG-specific outputs
357
+ outputs = result.outputs
358
+ answer = (
359
+ outputs.get("llm_response", {})
360
+ .get("choices", [{}])[0]
361
+ .get("message", {})
362
+ .get("content", "")
363
+ )
364
+ sources = outputs.get("relevant_chunks", [])
365
+
366
+ return RAGResponse(
367
+ answer=answer,
368
+ sources=sources,
369
+ query=request.query,
370
+ execution_time=time.time() - start_time,
371
+ )
372
+
373
+
374
+ # Factory function for creating API wrappers
375
+ def create_workflow_api(
376
+ workflow: WorkflowBuilder, api_type: str = "generic", **kwargs
377
+ ) -> WorkflowAPI:
378
+ """
379
+ Factory function to create appropriate API wrapper.
380
+
381
+ Args:
382
+ workflow: The workflow to wrap
383
+ api_type: Type of API wrapper ("generic", "rag", etc.)
384
+ **kwargs: Additional arguments for API initialization
385
+
386
+ Returns:
387
+ Configured WorkflowAPI instance
388
+
389
+ Example:
390
+ >>> api = create_workflow_api(my_workflow, api_type="rag")
391
+ >>> api.run(port=8000)
392
+ """
393
+ api_classes = {
394
+ "generic": WorkflowAPI,
395
+ "rag": HierarchicalRAGAPI,
396
+ }
397
+
398
+ api_class = api_classes.get(api_type, WorkflowAPI)
399
+ return api_class(workflow, **kwargs)
@@ -13,8 +13,8 @@ from .ai_providers import (
13
13
  get_available_providers,
14
14
  get_provider,
15
15
  )
16
- from .embedding_generator import EmbeddingGenerator
17
- from .llm_agent import LLMAgent
16
+ from .embedding_generator import EmbeddingGeneratorNode
17
+ from .llm_agent import LLMAgentNode
18
18
  from .models import (
19
19
  ModelPredictor,
20
20
  NamedEntityRecognizer,
@@ -30,9 +30,9 @@ __all__ = [
30
30
  "RetrievalAgent",
31
31
  "FunctionCallingAgent",
32
32
  "PlanningAgent",
33
- "LLMAgent",
33
+ "LLMAgentNode",
34
34
  # Embedding and Vector Operations
35
- "EmbeddingGenerator",
35
+ "EmbeddingGeneratorNode",
36
36
  # Provider Infrastructure
37
37
  "LLMProvider",
38
38
  "OllamaProvider",
@@ -333,7 +333,7 @@ class PlanningAgent(Node):
333
333
  # Data processing workflow
334
334
  potential_steps = [
335
335
  {
336
- "tool": "CSVReader",
336
+ "tool": "CSVReaderNode",
337
337
  "description": "Read input data",
338
338
  "parameters": {"file_path": "input.csv"},
339
339
  },
@@ -348,7 +348,7 @@ class PlanningAgent(Node):
348
348
  "parameters": {"group_by": "category", "operation": "sum"},
349
349
  },
350
350
  {
351
- "tool": "CSVWriter",
351
+ "tool": "CSVWriterNode",
352
352
  "description": "Write results",
353
353
  "parameters": {"file_path": "output.csv"},
354
354
  },
@@ -357,7 +357,7 @@ class PlanningAgent(Node):
357
357
  # Text analysis workflow
358
358
  potential_steps = [
359
359
  {
360
- "tool": "TextReader",
360
+ "tool": "TextReaderNode",
361
361
  "description": "Read text data",
362
362
  "parameters": {"file_path": "text.txt"},
363
363
  },
@@ -372,7 +372,7 @@ class PlanningAgent(Node):
372
372
  "parameters": {"max_length": 200},
373
373
  },
374
374
  {
375
- "tool": "JSONWriter",
375
+ "tool": "JSONWriterNode",
376
376
  "description": "Save analysis results",
377
377
  "parameters": {"file_path": "analysis.json"},
378
378
  },
@@ -1159,18 +1159,17 @@ def get_provider(
1159
1159
  ValueError: If the provider name is not recognized or doesn't support the requested type.
1160
1160
 
1161
1161
  Examples:
1162
-
1163
- Get any provider::
1164
-
1165
- provider = get_provider("openai")
1166
- if provider.supports_chat():
1167
- # Use for chat
1168
- if provider.supports_embeddings():
1169
- # Use for embeddings
1170
-
1171
- Get chat-only provider:
1172
-
1173
- chat_provider = get_provider("anthropic", "chat")
1162
+ >>> # Get any provider
1163
+ >>> provider = get_provider("openai")
1164
+ >>> if provider.supports_chat():
1165
+ ... # Use for chat
1166
+ ... pass
1167
+ >>> if provider.supports_embeddings():
1168
+ ... # Use for embeddings
1169
+ ... pass
1170
+
1171
+ >>> # Get chat-only provider
1172
+ >>> chat_provider = get_provider("anthropic", "chat")
1174
1173
  response = chat_provider.chat(messages, model="claude-3-sonnet")
1175
1174
 
1176
1175
  Get embedding-only provider:
@@ -1223,18 +1222,15 @@ def get_available_providers(
1223
1222
  Dict mapping provider names to their availability and capabilities.
1224
1223
 
1225
1224
  Examples:
1225
+ >>> # Get all providers
1226
+ >>> all_providers = get_available_providers()
1227
+ >>> for name, info in all_providers.items():
1228
+ ... print(f"{name}: Available={info['available']}, Chat={info['chat']}, Embeddings={info['embeddings']}")
1226
1229
 
1227
- Get all providers::
1228
-
1229
- all_providers = get_available_providers()
1230
- for name, info in all_providers.items():
1231
- print(f"{name}: Available={info['available']}, Chat={info['chat']}, Embeddings={info['embeddings']}")
1232
-
1233
- Get only chat providers:
1234
-
1235
- chat_providers = get_available_providers("chat")
1230
+ >>> # Get only chat providers
1231
+ >>> chat_providers = get_available_providers("chat")
1236
1232
 
1237
- Get only embedding providers:
1233
+ >>> # Get only embedding providers
1238
1234
 
1239
1235
  embed_providers = get_available_providers("embeddings")
1240
1236
  """
@@ -7,7 +7,7 @@ from kailash.nodes.base import Node, NodeParameter, register_node
7
7
 
8
8
 
9
9
  @register_node()
10
- class EmbeddingGenerator(Node):
10
+ class EmbeddingGeneratorNode(Node):
11
11
  """
12
12
  Vector embedding generator for RAG systems and semantic similarity operations.
13
13
 
@@ -61,46 +61,42 @@ class EmbeddingGenerator(Node):
61
61
  - Updates usage statistics and cost tracking
62
62
 
63
63
  Examples:
64
-
65
- Single text embedding::
66
-
67
- embedder = EmbeddingGenerator()
68
- result = embedder.run(
69
- provider="openai",
70
- model="text-embedding-3-large",
71
- input_text="This is a sample document to embed",
72
- operation="embed_text"
73
- )
74
-
75
- Batch document embedding:
76
-
77
- batch_embedder = EmbeddingGenerator()
78
- result = batch_embedder.run(
79
- provider="huggingface",
80
- model="sentence-transformers/all-MiniLM-L6-v2",
81
- input_texts=[
82
- "First document content...",
83
- "Second document content...",
84
- "Third document content..."
85
- ],
86
- operation="embed_batch",
87
- batch_size=32,
88
- cache_enabled=True
89
- )
90
-
91
- Similarity calculation:
92
-
93
- similarity = EmbeddingGenerator()
94
- result = similarity.run(
95
- operation="calculate_similarity",
96
- embedding_1=[0.1, 0.2, 0.3, ...],
97
- embedding_2=[0.15, 0.25, 0.35, ...],
98
- similarity_metric="cosine"
99
- )
64
+ >>> # Single text embedding
65
+ >>> embedder = EmbeddingGeneratorNode()
66
+ >>> result = embedder.run(
67
+ ... provider="openai",
68
+ ... model="text-embedding-3-large",
69
+ ... input_text="This is a sample document to embed",
70
+ ... operation="embed_text"
71
+ ... )
72
+
73
+ >>> # Batch document embedding
74
+ >>> batch_embedder = EmbeddingGeneratorNode()
75
+ >>> result = batch_embedder.run(
76
+ ... provider="huggingface",
77
+ ... model="sentence-transformers/all-MiniLM-L6-v2",
78
+ ... input_texts=[
79
+ ... "First document content...",
80
+ ... "Second document content...",
81
+ ... "Third document content..."
82
+ ... ],
83
+ ... operation="embed_batch",
84
+ ... batch_size=32,
85
+ ... cache_enabled=True
86
+ ... )
87
+
88
+ >>> # Similarity calculation
89
+ >>> similarity = EmbeddingGeneratorNode()
90
+ >>> result = similarity.run(
91
+ ... operation="calculate_similarity",
92
+ ... embedding_1=[0.1, 0.2, 0.3], # ... removed for doctest
93
+ ... embedding_2=[0.15, 0.25, 0.35], # ... removed for doctest
94
+ ... similarity_metric="cosine"
95
+ ... )
100
96
 
101
97
  Cached embedding with MCP integration:
102
98
 
103
- mcp_embedder = EmbeddingGenerator()
99
+ mcp_embedder = EmbeddingGeneratorNode()
104
100
  result = mcp_embedder.run(
105
101
  provider="azure",
106
102
  model="text-embedding-3-small",