Spaces:
Running
Running
RemanenetSpy
Rebrand to Kaal — rename from Chronos OS, add Logo component with serif seal design, swap all Hexagon icons for Logo
4303a4c | """ | |
| KAAL — Agent State Graph | |
| ================================ | |
| LangGraph state graph that orchestrates the Chronos agent. | |
| Architecture: retrieve_memory → call_model → (tools loop) → END | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import uuid | |
| from typing import Annotated, Any | |
| from langchain_core.messages import AIMessage, HumanMessage, BaseMessage | |
| from langgraph.graph import StateGraph, START, END | |
| from langgraph.graph.message import add_messages | |
| from typing_extensions import TypedDict | |
| from .nodes import ( | |
| retrieve_memory_node, | |
| call_model_node, | |
| execute_tools_node, | |
| ) | |
| logger = logging.getLogger("chronos.agent.graph") | |
| # --------------------------------------------------------------------------- | |
| # Agent State | |
| # --------------------------------------------------------------------------- | |
| class ChronosAgentState(TypedDict): | |
| """State shared across all nodes in the agent graph.""" | |
| messages: Annotated[list[BaseMessage], add_messages] | |
| memory_context: str | |
| source_ids: list[str] | |
| owner_id: str # Tenant isolation: only see own data | |
| tool_ids: list[str] | |
| events_retrieved: int | |
| events_created: int | |
| step_count: int | |
| max_steps: int | |
| # --------------------------------------------------------------------------- | |
| # Routing Logic | |
| # --------------------------------------------------------------------------- | |
| def should_continue(state: ChronosAgentState) -> str: | |
| """ | |
| Conditional edge: decide whether to execute tools or finish. | |
| Returns 'tools' if the last message has tool calls, else 'end'. | |
| """ | |
| messages = state.get("messages", []) | |
| step_count = state.get("step_count", 0) | |
| max_steps = state.get("max_steps", 10) | |
| # Safety: stop if we've exceeded max steps | |
| if step_count >= max_steps: | |
| logger.warning(f"Agent hit max steps ({max_steps}), stopping") | |
| return "end" | |
| if not messages: | |
| return "end" | |
| last_msg = messages[-1] | |
| if isinstance(last_msg, AIMessage) and last_msg.tool_calls: | |
| return "tools" | |
| return "end" | |
| async def increment_step(state: ChronosAgentState) -> ChronosAgentState: | |
| """Increment the step counter after tool execution.""" | |
| state["step_count"] = state.get("step_count", 0) + 1 | |
| return state | |
| # --------------------------------------------------------------------------- | |
| # Graph Construction | |
| # --------------------------------------------------------------------------- | |
| def build_agent_graph() -> StateGraph: | |
| """ | |
| Build the Chronos agent state graph. | |
| Flow: | |
| START → retrieve_memory → call_model → [tools → call_model]* → END | |
| """ | |
| workflow = StateGraph(ChronosAgentState) | |
| # Add nodes | |
| workflow.add_node("retrieve_memory", retrieve_memory_node) | |
| workflow.add_node("call_model", call_model_node) | |
| workflow.add_node("tools", execute_tools_node) | |
| workflow.add_node("increment", increment_step) | |
| # Add edges | |
| workflow.add_edge(START, "retrieve_memory") | |
| workflow.add_edge("retrieve_memory", "call_model") | |
| # Conditional: after model, either use tools or finish | |
| workflow.add_conditional_edges( | |
| "call_model", | |
| should_continue, | |
| { | |
| "tools": "tools", | |
| "end": END, | |
| }, | |
| ) | |
| # After tools, increment step count then go back to model | |
| workflow.add_edge("tools", "increment") | |
| workflow.add_edge("increment", "call_model") | |
| return workflow | |
| # Compiled graph (singleton) | |
| _graph = None | |
| def get_agent_graph(): | |
| """Get or create the compiled agent graph.""" | |
| global _graph | |
| if _graph is None: | |
| workflow = build_agent_graph() | |
| _graph = workflow.compile() | |
| logger.info("Chronos agent graph compiled successfully") | |
| return _graph | |
| # --------------------------------------------------------------------------- | |
| # Public API | |
| # --------------------------------------------------------------------------- | |
| async def run_agent_graph( | |
| prompt: str, | |
| thread_id: str | None = None, | |
| source_ids: list[str] | None = None, | |
| tool_ids: list[str] | None = None, | |
| max_steps: int = 10, | |
| owner_id: str = "", | |
| ) -> dict[str, Any]: | |
| """ | |
| Run the Chronos agent with temporal memory. | |
| Args: | |
| prompt: The user's task/question | |
| thread_id: Session ID for continuity (None = new session) | |
| source_ids: Memory scopes to search | |
| tool_ids: Additional connector tool IDs to enable | |
| max_steps: Maximum tool-use iterations | |
| owner_id: API key owner for tenant-isolated memory access | |
| Returns: | |
| dict with: response, steps, events_retrieved, events_created | |
| """ | |
| graph = get_agent_graph() | |
| tid = thread_id or uuid.uuid4().hex | |
| initial_state: ChronosAgentState = { | |
| "messages": [HumanMessage(content=prompt)], | |
| "memory_context": "", | |
| "source_ids": source_ids or [], | |
| "owner_id": owner_id, | |
| "tool_ids": tool_ids or [], | |
| "events_retrieved": 0, | |
| "events_created": 0, | |
| "step_count": 0, | |
| "max_steps": max_steps, | |
| } | |
| config = {"configurable": {"thread_id": tid}} | |
| try: | |
| result = await graph.ainvoke(initial_state, config=config) | |
| # Extract final response | |
| messages = result.get("messages", []) | |
| final_response = "" | |
| for msg in reversed(messages): | |
| if isinstance(msg, AIMessage) and msg.content and not msg.tool_calls: | |
| if isinstance(msg.content, list): | |
| text_parts = [] | |
| for block in msg.content: | |
| if isinstance(block, dict): | |
| # Handle GLM/DeepSeek reasoning block formats | |
| if block.get("type") == "text" and "text" in block: | |
| text_parts.append(block["text"]) | |
| # Some models put text directly in 'text' without type | |
| elif "text" in block and not block.get("type") == "thinking": | |
| text_parts.append(block["text"]) | |
| elif isinstance(block, str): | |
| text_parts.append(block) | |
| final_response = "\n".join(text_parts) | |
| else: | |
| final_response = str(msg.content) | |
| break | |
| # Build step log | |
| steps = [] | |
| for i, msg in enumerate(messages): | |
| if isinstance(msg, AIMessage): | |
| content_str = str(msg.content) if msg.content else "" | |
| step = {"type": "ai", "content": content_str[:200]} | |
| if msg.tool_calls: | |
| step["tool_calls"] = [ | |
| {"name": tc["name"], "args": tc["args"]} | |
| for tc in msg.tool_calls | |
| ] | |
| steps.append(step) | |
| return { | |
| "response": final_response, | |
| "thread_id": tid, | |
| "steps": steps, | |
| "events_retrieved": result.get("events_retrieved", 0), | |
| "events_created": result.get("events_created", 0), | |
| } | |
| except Exception as e: | |
| logger.error(f"Agent graph execution failed: {e}") | |
| return { | |
| "response": f"Agent error: {str(e)}", | |
| "thread_id": tid, | |
| "steps": [{"type": "error", "message": str(e)}], | |
| "events_retrieved": 0, | |
| "events_created": 0, | |
| } | |