11import os
2+ from contextlib import asynccontextmanager
3+
24import redis .asyncio as redis
35from dotenv import load_dotenv
46from fastapi import FastAPI , HTTPException
57from pydantic import BaseModel
68from temporalio .client import Client
79from temporalio .contrib .pydantic import pydantic_data_converter
10+
11+ from kin .models .schemas import TaskResult
12+ from kin .observability .logging import get_logger , setup_logging
813from kin .orchestrator .executor .dag_workflow import KinDAGWorkflow
9- from kin .models .schemas import DAGSpec , TaskNode , TaskResult
1014from kin .orchestrator .planner import Planner
1115
1216load_dotenv ()
17+ setup_logging ()
18+ log = get_logger ("kin.gateway" )
19+
20+ RESULTS_TTL_SEC = 3600
21+
22+
23+ @asynccontextmanager
24+ async def lifespan (app : FastAPI ):
25+ log .info ("Gateway starting up..." )
26+ app .state .temporal = await Client .connect (
27+ "localhost:7233" , data_converter = pydantic_data_converter
28+ )
29+ app .state .redis = redis .Redis (host = "localhost" , port = 6379 , decode_responses = True )
30+ log .info ("Connected to Temporal and Redis" )
31+ yield
32+ await app .state .redis .aclose ()
33+ log .info ("Gateway shut down cleanly" )
1334
14- app = FastAPI (title = "Kin AI Gateway" )
35+
36+ app = FastAPI (title = "Kin AI Gateway" , lifespan = lifespan )
1537
1638
1739class WorkflowRequest (BaseModel ):
1840 prompt : str
1941
2042
43+ # ---------------------------------------------------------------------------
44+ # Health check
45+ # ---------------------------------------------------------------------------
46+ @app .get ("/healthz" , tags = ["ops" ])
47+ async def health ():
48+ """Liveness probe — returns 200 when gateway is up and Redis is reachable."""
49+ try :
50+ await app .state .redis .ping ()
51+ redis_ok = True
52+ except Exception :
53+ redis_ok = False
54+ return {"status" : "ok" , "redis" : redis_ok }
55+
56+
57+ # ---------------------------------------------------------------------------
58+ # Workflow endpoints
59+ # ---------------------------------------------------------------------------
2160@app .post ("/v1/workflows" )
2261async def start_workflow (request : WorkflowRequest ):
2362 try :
24- client = await Client .connect (
25- "localhost:7233" , data_converter = pydantic_data_converter
26- )
27-
28- # NEW: use planner
2963 planner = Planner (api_key = os .getenv ("GROQ_API_KEY" ))
3064 dag = planner .plan (request .prompt )
31-
3265 dag_id = str (dag .workflow_id )
3366
34- await client .start_workflow (
67+ log .info ("Starting workflow dag_id=%s nodes=%d" , dag_id , len (dag .nodes ))
68+
69+ await app .state .temporal .start_workflow (
3570 KinDAGWorkflow .run ,
3671 dag ,
3772 id = dag_id ,
@@ -44,29 +79,22 @@ async def start_workflow(request: WorkflowRequest):
4479 }
4580
4681 except Exception as e :
47- print ( f"Error starting workflow: { e } " )
82+ log . error ( "Failed to start workflow: %s" , e , exc_info = True )
4883 raise HTTPException (status_code = 500 , detail = str (e ))
4984
5085
51- # Initialize Redis client (ideally outside the function or in app state)
52- redis_client = redis .Redis (host = "localhost" , port = 6379 , decode_responses = True )
53-
54-
5586@app .get ("/v1/workflows/{workflow_id}" )
5687async def get_status (workflow_id : str ):
5788 try :
58- client = await Client .connect ("localhost:7233" )
59- handle = client .get_workflow_handle (workflow_id )
89+ handle = app .state .temporal .get_workflow_handle (workflow_id )
6090 desc = await handle .describe ()
6191
6292 stream_key = f"results:{ workflow_id } "
63- raw_entries = await redis_client .xrange (stream_key )
64-
65- final_results = {}
93+ raw_entries = await app .state .redis .xrange (stream_key )
6694
95+ final_results : dict = {}
6796 for _ , entry in raw_entries :
6897 result = TaskResult .model_validate_json (entry ["data" ])
69-
7098 final_results [result .node_id ] = {
7199 "status" : result .status ,
72100 "agent_type" : (
@@ -76,18 +104,24 @@ async def get_status(workflow_id: str):
76104 "error" : result .error ,
77105 }
78106
107+ overall_status = desc .status .name
108+
109+ # Set TTL on result stream once workflow is terminal
110+ if overall_status in ("COMPLETED" , "FAILED" , "TERMINATED" , "TIMED_OUT" ):
111+ await app .state .redis .expire (stream_key , RESULTS_TTL_SEC )
112+
79113 return {
80114 "workflow_id" : workflow_id ,
81- "status" : desc . status . name ,
115+ "status" : overall_status ,
82116 "results" : final_results ,
83117 }
84118
85119 except Exception as e :
120+ log .warning ("get_status error for %s: %s" , workflow_id , e )
86121 raise HTTPException (status_code = 404 , detail = str (e ))
87122
88123
89124if __name__ == "__main__" :
90125 import uvicorn
91126
92- # Use the string "kin.gateway.main:app" for hot-reloading support
93- uvicorn .run (app , host = "0.0.0.0" , port = 8000 )
127+ uvicorn .run ("kin.gateway.main:app" , host = "0.0.0.0" , port = 8000 , reload = True )
0 commit comments