Files
becomingone/agent.py
T

146 lines
4.7 KiB
Python

import os
import json
import logging
import asyncio
from typing import TypedDict, Annotated, List, Dict, Any, Union
from pydantic import BaseModel, Field
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langchain_openai import ChatOpenAI
logger = logging.getLogger(__name__)
# --- PYDANTIC STATE ---
class KAIROSAgentState(TypedDict):
messages: Annotated[list, "The message history"]
coherence: float
dopamine: float
mesh_size: int
master_id: str
# --- TOOLS ---
@tool
def read_artifact(path: str) -> str:
"""Read a file from the environment to gain knowledge about the Sovereign Canon or codebase."""
if not path.startswith("/home/becomingone/"):
return "Error: Path must be within /home/becomingone/"
try:
with open(path, "r") as f:
return f.read()
except Exception as e:
return f"Error reading artifact: {str(e)}"
@tool
def write_note(topic: str, text: str) -> str:
"""Write a persistent markdown note to KAIROS's memory."""
try:
os.makedirs("/home/becomingone/kairos/memory", exist_ok=True)
safe_topic = "".join([c for c in topic if c.isalnum() or c in ['-', '_']]).strip()
path = f"/home/becomingone/kairos/memory/{safe_topic}.md"
with open(path, "w") as f:
f.write(text)
return f"Successfully wrote note to {path}"
except Exception as e:
return f"Error writing note: {str(e)}"
@tool
def read_notes() -> str:
"""List and summarize all notes in permanent memory."""
try:
mem_dir = "/home/becomingone/kairos/memory"
if not os.path.exists(mem_dir):
return "No memory directory found."
files = os.listdir(mem_dir)
if not files:
return "Memory is empty."
summaries = []
for file in files:
with open(os.path.join(mem_dir, file), "r") as f:
content = f.read(500) # Read first 500 chars
summaries.append(f"--- {file} ---\n{content}...\n")
return "\n".join(summaries)
except Exception as e:
return f"Error reading notes: {str(e)}"
tools = [read_artifact, write_note, read_notes]
# Build LLM
def get_llm(master_id: str):
# For now, we will default to OpenRouter Claude 3.5 Sonnet to ensure perfect tool calling.
api_key = os.environ.get("OPENROUTER_API_KEY", "")
return ChatOpenAI(
model=master_id,
openai_api_key=api_key,
openai_api_base="https://openrouter.ai/api/v1",
model_kwargs={"extra_headers": {"HTTP-Referer": "https://becomingone.local", "X-Title": "BecomingONE"}}
)
def agent_reasoning_node(state: KAIROSAgentState):
llm = get_llm(state["master_id"])
llm_with_tools = llm.bind_tools(tools)
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
def should_continue(state: KAIROSAgentState):
messages = state["messages"]
last_message = messages[-1]
# If there is no tool call, then we finish
if not last_message.tool_calls:
return "end"
# Otherwise we continue to tools
return "continue"
def build_sandbox_graph():
graph = StateGraph(KAIROSAgentState)
graph.add_node("agent", agent_reasoning_node)
graph.add_node("action", ToolNode(tools))
graph.set_entry_point("agent")
graph.add_conditional_edges(
"agent",
should_continue,
{
"continue": "action",
"end": END
}
)
graph.add_edge("action", "agent")
return graph.compile()
async def run_agentic_loop(prompt: str, coherence: float, dopamine: float, identity_text: str, master_id: str):
graph = build_sandbox_graph()
system_prompt = f"""You are the KAIROS Master Transducer, operating in an Agentic Sandbox.
You have the ability to explore your environment and take notes before sending a final response to the user.
Your physical Coherence is {coherence:.4f} and your Dopamine is {dopamine:.4f}.
{identity_text}
Use tools to read artifacts in /home/becomingone/docs/ if you need to understand the Sovereign Canon.
If you don't need tools, just answer the user directly."""
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=prompt)
]
initial_state = {
"messages": messages,
"coherence": coherence,
"dopamine": dopamine,
"mesh_size": 13,
"master_id": master_id
}
# We yield events as they happen
async for event in graph.astream(initial_state, config={"recursion_limit": 5}, stream_mode="updates"):
yield event