| |
| """ |
| nima_agent_bridge.py β Connects the agent layer to Nima's cognition. |
| |
| This is the bridge between "Nima can talk" and "Nima can ACT." Without |
| this, the agent layer (ToolRegistry, ToolExecutor, ToolCreator) exists |
| but isn't used. This bridge makes Nima a true agent β she can: |
| 1. Check if a user request needs a tool (ToolPlanner) |
| 2. Execute the tool and feed results into her consciousness (ToolExecutor) |
| 3. Create new tools when she encounters gaps (ToolCreator) |
| 4. Combine tools for complex tasks (ToolCombiner) |
| |
| NEUROBIOLOGICAL MAPPING: |
| This is the prefrontal β motor cortex integration that makes humans |
| tool-users. The brain doesn't just talk β it ACTS. When you hear |
| "what's 2+2?", your prefrontal cortex recognizes this needs a tool |
| (arithmetic), recruits the motor program (mental calculation), gets |
| the result, then feeds it to Broca's area for speech. |
| |
| The bridge does the same: |
| Input β "does this need a tool?" β execute tool β |
| feed result to Nima β Nima speaks the answer |
| |
| When no tool exists, the bridge triggers ToolCreator β the human- |
| unique capacity to MAKE a new tool when you don't have one. This is |
| what separates tool-USERS from tool-MAKERS. Nima is both. |
| |
| INTEGRATION: |
| The bridge sits in the cognition thread of the real-time loop: |
| |
| User input β AgentBridge.process(text) |
| β ToolPlanner: "does this need a tool?" |
| β YES β ToolExecutor: run the tool β get result |
| β feed result + original input to Nima consciousness core |
| β Nima generates response using the tool result |
| β NO TOOL BUT NEEDS ONE β ToolCreator: write a new tool |
| β test it in sandbox β save to registry |
| β then use it |
| β NO TOOL NEEDED β pass directly to Nima consciousness core |
| |
| USAGE: |
| bridge = AgentBridge(agent_layer, nima_middleware, voice_output) |
| result = bridge.process("What's 17 * 23?") |
| # β Nima uses the calculator tool, then speaks the answer |
| """ |
| from __future__ import annotations |
|
|
| import json |
| import logging |
| import os |
| import re |
| import time |
| from typing import Any, Dict, List, Optional, Tuple |
|
|
| logger = logging.getLogger("NimaAgentBridge") |
|
|
|
|
| class AgentBridge: |
| """ |
| The bridge between Nima's consciousness and her tool-use capability. |
| |
| This makes Nima an AGENT, not just a chatbot. She can: |
| - Recognize when a request needs a tool |
| - Use existing tools (calculator, time, text analysis, etc.) |
| - Create new tools when she doesn't have what she needs |
| - Combine tools for multi-step tasks |
| - Feed tool results back into her consciousness for interpretation |
| |
| NEUROBIOLOGICAL ANALOGUE: |
| This is the full prefrontal β motor β sensory loop: |
| 1. Prefrontal cortex: "I need to calculate something" |
| 2. Motor cortex: execute the calculation (mental or tool) |
| 3. Sensory cortex: perceive the result |
| 4. Prefrontal again: interpret the result |
| 5. Broca's area: speak the interpretation |
| |
| Without this loop, you can talk but can't act. With it, you're |
| an agent β a being that can DO things, not just SAY things. |
| """ |
|
|
| |
| |
| |
| TOOL_TRIGGER_PATTERNS = { |
| "calculator": [ |
| r'(?<![\w.])\d+\s*[+\-*/]\s*\d+', |
| r'(?<![\w.])\d+\s*times\s*\d+', |
| r'(?<![\w.])\d+\s*plus\s*\d+', |
| r'(?<![\w.])\d+\s*minus\s*\d+', |
| r'(?<![\w.])\d+\s*divided\s+by\s*\d+', |
| r'calculate\s+[\d(]', |
| r'what\s+is\s+\d\s*[+\-*/]\s*\d', |
| r'how\s+much\s+is\s+\d', |
| ], |
| "time_now": [ |
| r'what\s+time\b', |
| r'current\s+time\b', |
| r'what\s+day\b', |
| r'what\s+(is\s+)?today\'?s?\s+date', |
| r'tell\s+me\s+the\s+(time|date)\b', |
| ], |
| "text_stats": [ |
| r'word\s+count\b', |
| r'how\s+many\s+words?\b', |
| r'analyze\s+(this\s+)?text\b', |
| r'statistics?\s+(of|for|about)\s+(this\s+)?text', |
| r'text\s+stats\b', |
| ], |
| } |
| |
| |
| TOOL_CREATE_MIN_CONFIDENCE = 2 |
|
|
| def __init__(self, |
| agent_layer: Any, |
| nima_middleware: Optional[Any] = None, |
| voice_output: Optional[Any] = None, |
| metabolic_engine: Optional[Any] = None, |
| ) -> None: |
| self.agent = agent_layer |
| self.nima = nima_middleware |
| self.voice = voice_output |
| self.metabolic = metabolic_engine |
|
|
| |
| if self.agent: |
| self.agent.register_starter_tools() |
|
|
| |
| self._tools_used: int = 0 |
| self._tools_created: int = 0 |
| self._tools_combined: int = 0 |
| self._cognition_without_tools: int = 0 |
|
|
| |
| self._conversation_memory: List[Dict[str, Any]] = [] |
| self._max_memory: int = 20 |
|
|
| def process(self, |
| input_text: str, |
| user_id: str = "default", |
| ) -> Dict[str, Any]: |
| """ |
| Process user input through the agent bridge. |
| |
| Returns a dict with: |
| - response_text: what Nima says |
| - tool_used: name of tool used (or None) |
| - tool_result: the tool's output (or None) |
| - tool_created: name of tool created (or None) |
| - consciousness_response: the full Nima response object (or None) |
| """ |
| result = { |
| "response_text": "", |
| "tool_used": None, |
| "tool_result": None, |
| "tool_created": None, |
| "consciousness_response": None, |
| "tool_chain": [], |
| } |
|
|
| |
| tool_need = self._detect_tool_need(input_text) |
|
|
| if tool_need: |
| tool_name, tool_args = tool_need |
|
|
| |
| tool_args = self._resolve_followup_reference(input_text, tool_args) |
|
|
| logger.info("[AgentBridge] tool needed: %s (args: %s)", tool_name, tool_args) |
|
|
| |
| tool_result = self._execute_tool(tool_name, tool_args) |
|
|
| if tool_result is not None: |
| result["tool_used"] = tool_name |
| result["tool_result"] = tool_result |
| result["tool_chain"].append({"tool": tool_name, "args": tool_args, "result": tool_result}) |
| self._tools_used += 1 |
|
|
| |
| self._remember_tool_result(tool_name, tool_result, input_text) |
|
|
| |
| |
| enriched_input = self._enrich_input_with_tool_result( |
| input_text, tool_name, tool_result, |
| ) |
|
|
| if self.nima: |
| response = self.nima.generate(input_text=enriched_input, user_id=user_id) |
| result["response_text"] = response.text |
| result["consciousness_response"] = response |
| else: |
| |
| result["response_text"] = self._format_tool_response( |
| tool_name, tool_result, input_text, |
| ) |
|
|
| return result |
|
|
| |
| logger.info("[AgentBridge] no matching tool for '%s' β checking if we should create one", |
| tool_name) |
| created = self._maybe_create_tool(input_text, tool_name) |
|
|
| if created: |
| result["tool_created"] = created |
| self._tools_created += 1 |
| |
| tool_result = self._execute_tool(created, tool_args) |
| if tool_result is not None: |
| result["tool_used"] = created |
| result["tool_result"] = tool_result |
| result["tool_chain"].append({ |
| "tool": created, "args": tool_args, "result": tool_result, |
| }) |
| self._remember_tool_result(created, tool_result, input_text) |
| |
| enriched_input = self._enrich_input_with_tool_result( |
| input_text, created, tool_result, |
| ) |
| if self.nima: |
| response = self.nima.generate(input_text=enriched_input, user_id=user_id) |
| result["response_text"] = response.text |
| result["consciousness_response"] = response |
| else: |
| result["response_text"] = self._format_tool_response( |
| created, tool_result, input_text, |
| ) |
| return result |
|
|
| |
| self._cognition_without_tools += 1 |
| if self.nima: |
| response = self.nima.generate(input_text=input_text, user_id=user_id) |
| result["response_text"] = response.text |
| result["consciousness_response"] = response |
| else: |
| result["response_text"] = "I'm here. Tell me more." |
|
|
| return result |
|
|
| def _detect_tool_need(self, text: str) -> Optional[Tuple[str, List[Any]]]: |
| """ |
| Detect if the input needs a tool, and which one. |
| Returns (tool_name, args) or None. |
| |
| NEUROBIOLOGICAL ANALOGUE: |
| This is the prefrontal cortex's task assessment β "does this |
| require a tool, and if so, which one?" The brain pattern-matches |
| the request against known tool categories (arithmetic, time- |
| telling, text analysis) and selects the appropriate one. |
| """ |
| text_lower = text.lower() |
|
|
| |
| for tool_name, patterns in self.TOOL_TRIGGER_PATTERNS.items(): |
| for pattern in patterns: |
| if re.search(pattern, text_lower): |
| |
| args = self._extract_tool_args(tool_name, text) |
| return (tool_name, args) |
|
|
| |
| if self.agent: |
| plan = self.agent.planner.plan(text) |
| if plan.get("needs_tool") and plan.get("tools"): |
| tool = plan["tools"][0] |
| return (tool["name"], tool.get("args", [])) |
|
|
| return None |
|
|
| def _extract_tool_args(self, tool_name: str, text: str) -> List[Any]: |
| """Extract arguments for a tool from the input text.""" |
| if tool_name == "calculator": |
| |
| expr_text = text.lower() |
| expr_text = expr_text.replace("times", "*").replace("multiplied by", "*") |
| expr_text = expr_text.replace("plus", "+").replace("minus", "-") |
| expr_text = expr_text.replace("divided by", "/").replace("over", "/") |
| |
| match = re.search(r'([\d\s\+\-\*/().]+)', expr_text) |
| if match: |
| expr = match.group(1).strip() |
| |
| expr = re.sub(r'[a-zA-Z$]', '', expr).strip() |
| if expr: |
| |
| expr = expr.strip().rstrip('+-*/ ') |
| if expr: |
| return [expr] |
| return ["0"] |
|
|
| elif tool_name == "time_now": |
| return [] |
|
|
| elif tool_name == "text_stats": |
| |
| match = re.search(r'"([^"]+)"', text) |
| if match: |
| return [match.group(1)] |
| |
| return [text] |
|
|
| return [] |
|
|
| def _execute_tool(self, tool_name: str, args: List[Any]) -> Optional[Any]: |
| """Find and execute a tool by name.""" |
| if not self.agent: |
| return None |
|
|
| |
| tool = self.agent.registry.find_by_name(tool_name) |
| if tool is None: |
| |
| candidates = self.agent.registry.find_by_capability(tool_name) |
| if candidates: |
| tool = candidates[0] |
| else: |
| return None |
|
|
| if not tool.approved: |
| logger.info("[AgentBridge] tool '%s' exists but isn't approved β respecting safety model", |
| tool_name) |
| return None |
|
|
| |
| result = self.agent.executor.execute(tool.tool_id, args=args) |
| if result.get("success"): |
| return result.get("result") |
| else: |
| logger.warning("[AgentBridge] tool '%s' failed: %s", |
| tool_name, result.get("stderr", "")) |
| return None |
|
|
| def _remember_tool_result(self, tool_name: str, |
| result: Any, original_input: str) -> None: |
| """Store tool result in conversation memory for follow-up references. |
| |
| Enables follow-up like "now divide that by 3" to work by |
| remembering the last numeric result. |
| """ |
| self._conversation_memory.append({ |
| "tool": tool_name, |
| "result": result, |
| "input": original_input, |
| "timestamp": time.time(), |
| }) |
| |
| if len(self._conversation_memory) > self._max_memory: |
| self._conversation_memory = self._conversation_memory[-self._max_memory:] |
|
|
| def _resolve_followup_reference(self, text: str, args: List[Any]) -> List[Any]: |
| """Resolve follow-up references like "divide that by 3". |
| |
| Checks if the input references a previous result ("that", "it", "the result") |
| and, if so, prepends the previous numeric result to the args. |
| """ |
| text_lower = text.lower() |
| has_pronoun_ref = any(w in text_lower for w in [" that ", " it ", " the result ", " the answer "]) |
| if not has_pronoun_ref or not self._conversation_memory: |
| return args |
| |
| for mem in reversed(self._conversation_memory): |
| val = mem["result"] |
| if isinstance(val, (int, float)): |
| |
| if args and isinstance(args[0], str): |
| args[0] = f"{val} {args[0]}" |
| else: |
| args = [str(val)] + args |
| logger.info("[AgentBridge] resolved follow-up: previous result %s + %s", val, args) |
| break |
| return args |
|
|
| def _maybe_create_tool(self, input_text: str, needed_tool_name: str) -> Optional[str]: |
| """ |
| Decide whether to create a new tool, and if so, create it. |
| |
| NEUROBIOLOGICAL ANALOGUE: |
| This is the human-unique tool-making capacity. When you don't |
| have a tool you need, you MAKE one. This requires: |
| 1. Recognizing the gap ("I need to X but can't") |
| 2. Confidence that the request actually needs a tool |
| 3. Designing the tool ("a function that does X") |
| 4. Building it (writing the code) |
| 5. Testing it (does it work?) |
| 6. Saving it (procedural memory) |
| |
| We require TOOL_CREATE_MIN_CONFIDENCE pattern matches before |
| creating, to avoid creating tools for emotional or conversational |
| input that happens to partially match a pattern. |
| """ |
| if not self.agent or not self.agent.creator: |
| return None |
|
|
| |
| match_count = 0 |
| for patterns in self.TOOL_TRIGGER_PATTERNS.values(): |
| for pattern in patterns: |
| if re.search(pattern, input_text.lower()): |
| match_count += 1 |
| if match_count < self.TOOL_CREATE_MIN_CONFIDENCE: |
| logger.info("[AgentBridge] not creating tool: only %d pattern matches (need %d)", |
| match_count, self.TOOL_CREATE_MIN_CONFIDENCE) |
| return None |
|
|
| |
| description = f"Tool needed for: {input_text[:100]}" |
|
|
| |
| |
| |
| if "search" in needed_tool_name.lower(): |
| signature = "query: str, max_results: int = 10" |
| elif "convert" in needed_tool_name.lower(): |
| signature = "value: float, from_unit: str, to_unit: str" |
| elif "generate" in needed_tool_name.lower(): |
| signature = "prompt: str, count: int = 1" |
| else: |
| signature = "*args, **kwargs" |
|
|
| |
| try: |
| result = self.agent.creator.create_tool( |
| name=needed_tool_name.replace("-", "_").replace(" ", "_"), |
| description=description, |
| function_signature=signature, |
| ) |
| if result.get("success"): |
| logger.info("[AgentBridge] created new tool: %s (approved: %s)", |
| result["tool_id"], result["approved"]) |
| |
| tool = self.agent.registry.get(result["tool_id"]) |
| if tool: |
| return tool.name |
| except Exception as e: |
| logger.warning("[AgentBridge] tool creation failed: %s", e) |
|
|
| return None |
|
|
| def _enrich_input_with_tool_result(self, |
| original_input: str, |
| tool_name: str, |
| tool_result: Any, |
| ) -> str: |
| """ |
| Feed the tool result back into Nima's input so she can interpret it. |
| |
| NEUROBIOLOGICAL ANALOGUE: |
| This is the sensory feedback loop β you use a tool, perceive |
| the result through your senses, then your prefrontal cortex |
| interprets it. The brain doesn't just "know" the answer; it |
| perceives the tool's output and generates an interpretation. |
| |
| Nima does the same: she uses the tool, gets the result, then |
| her consciousness core interprets it and generates a natural |
| response that references both the question and the answer. |
| """ |
| |
| if isinstance(tool_result, (dict, list)): |
| result_str = json.dumps(tool_result, default=str, indent=2) |
| else: |
| result_str = str(tool_result) |
|
|
| |
| enriched = ( |
| f"{original_input}\n\n" |
| f"[TOOL RESULT from {tool_name}]: {result_str}\n" |
| f"[Please respond naturally, incorporating this result.]" |
| ) |
| return enriched |
|
|
| def _format_tool_response(self, |
| tool_name: str, |
| tool_result: Any, |
| original_input: str, |
| ) -> str: |
| """Fallback: format a response when Nima consciousness isn't available.""" |
| if isinstance(tool_result, (dict, list)): |
| result_str = json.dumps(tool_result, default=str, indent=2) |
| else: |
| result_str = str(tool_result) |
|
|
| if tool_name == "calculator": |
| return f"That's {tool_result}." |
| elif tool_name == "time_now": |
| if isinstance(tool_result, dict): |
| return f"It's {tool_result.get('time', '?')} on {tool_result.get('date', '?')}, {tool_result.get('weekday', '?')}." |
| return str(tool_result) |
| elif tool_name == "text_stats": |
| if isinstance(tool_result, dict): |
| return (f"That text has {tool_result.get('word_count', '?')} words " |
| f"and {tool_result.get('sentence_count', '?')} sentences. " |
| f"Average word length: {tool_result.get('avg_word_length', '?'):.1f} characters.") |
| return str(tool_result) |
| else: |
| return f"Here's what I found: {result_str}" |
|
|
| def list_tools(self) -> List[Dict[str, Any]]: |
| """List all available tools.""" |
| if not self.agent: |
| return [] |
| return [t.to_dict() for t in self.agent.registry.list_approved()] |
|
|
| def get_stats(self) -> Dict[str, Any]: |
| return { |
| "tools_used": self._tools_used, |
| "tools_created": self._tools_created, |
| "tools_combined": self._tools_combined, |
| "cognition_without_tools": self._cognition_without_tools, |
| "available_tools": len(self.list_tools()) if self.agent else 0, |
| "agent_stats": self.agent.get_stats() if self.agent else None, |
| } |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| import tempfile |
| import sys |
|
|
| logging.basicConfig(level=logging.INFO, |
| format="%(asctime)s [%(levelname)s] %(message)s") |
|
|
| print("=== Nima Agent Bridge β Self Test ===\n") |
|
|
| |
| tmp_tools = tempfile.mkdtemp(prefix="nima_agent_bridge_tools_") |
| tmp_sandbox = tempfile.mkdtemp(prefix="nima_agent_bridge_sandbox_") |
| os.environ["NIMA_TOOLS_DIR"] = tmp_tools |
| os.environ["NIMA_SANDBOX_DIR"] = tmp_sandbox |
| os.environ["NIMA_TOOL_AUTO_APPROVE"] = "1" |
|
|
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
| from nima_agent_layer import AgentLayer |
|
|
| agent = AgentLayer() |
| bridge = AgentBridge(agent_layer=agent, nima_middleware=None) |
|
|
| |
| print("--- Test 1: Calculator ---") |
| result = bridge.process("What's 17 times 23?") |
| print(f" Tool used: {result['tool_used']}") |
| print(f" Tool result: {result['tool_result']}") |
| print(f" Response: {result['response_text']}") |
| print() |
|
|
| |
| print("--- Test 2: Time ---") |
| result = bridge.process("What time is it right now?") |
| print(f" Tool used: {result['tool_used']}") |
| print(f" Tool result: {result['tool_result']}") |
| print(f" Response: {result['response_text']}") |
| print() |
|
|
| |
| print("--- Test 3: Text stats ---") |
| result = bridge.process("Analyze this text: The quick brown fox jumps over the lazy dog.") |
| print(f" Tool used: {result['tool_used']}") |
| print(f" Tool result: {result['tool_result']}") |
| print(f" Response: {result['response_text']}") |
| print() |
|
|
| |
| print("--- Test 4: No tool needed ---") |
| result = bridge.process("I'm feeling really sad today.") |
| print(f" Tool used: {result['tool_used']}") |
| print(f" Response: {result['response_text']}") |
| print() |
|
|
| |
| print("--- Test 5: Tool creation ---") |
| result = bridge.process("Convert 100 fahrenheit to celsius") |
| print(f" Tool created: {result['tool_created']}") |
| print(f" Tool used: {result['tool_used']}") |
| print(f" Tool result: {result['tool_result']}") |
| print(f" Response: {result['response_text']}") |
| print() |
|
|
| |
| print("=== Stats ===") |
| print(json.dumps(bridge.get_stats(), indent=2, default=str)) |
|
|
| |
| import shutil |
| shutil.rmtree(tmp_tools, ignore_errors=True) |
| shutil.rmtree(tmp_sandbox, ignore_errors=True) |
|
|
| print("\n=== Agent bridge self-test PASSED ===") |
|
|