File size: 25,104 Bytes
61848b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
#!/usr/bin/env python3
"""
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.
    """

    # Patterns that suggest tool use is needed.
    # NOTE: These are HEURISTICS. Each pattern is designed to minimize
    # false positives by requiring explicit mathematical/structural signals.
    TOOL_TRIGGER_PATTERNS = {
        "calculator": [
            r'(?<![\w.])\d+\s*[+\-*/]\s*\d+',  # "2+2", "17 * 23" (not phone numbers)
            r'(?<![\w.])\d+\s*times\s*\d+',      # "5 times 3"
            r'(?<![\w.])\d+\s*plus\s*\d+',        # "5 plus 3"
            r'(?<![\w.])\d+\s*minus\s*\d+',       # "10 minus 4"
            r'(?<![\w.])\d+\s*divided\s+by\s*\d+',  # "100 divided by 5"
            r'calculate\s+[\d(]',                    # "calculate 5*(3+2)"
            r'what\s+is\s+\d\s*[+\-*/]\s*\d',     # "what is 2+2"
            r'how\s+much\s+is\s+\d',                # "how much is 17*23"
        ],
        "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',
        ],
    }
    # Threshold for tool creation: need at least this many pattern matches
    # to trigger creation, avoiding creating tools for emotional input.
    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

        # Ensure starter tools are registered
        if self.agent:
            self.agent.register_starter_tools()

        # Stats
        self._tools_used: int = 0
        self._tools_created: int = 0
        self._tools_combined: int = 0
        self._cognition_without_tools: int = 0

        # Conversation memory: remember tool results for follow-up references
        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": [],
        }

        # Step 1: Check if a tool is needed
        tool_need = self._detect_tool_need(input_text)

        if tool_need:
            tool_name, tool_args = tool_need

            # Resolve follow-up references ("divide that by 3" β†’ prepend previous result)
            tool_args = self._resolve_followup_reference(input_text, tool_args)

            logger.info("[AgentBridge] tool needed: %s (args: %s)", tool_name, tool_args)

            # Step 2: Try to find and execute the tool
            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

                # Remember this for follow-up references ("divide that by 3")
                self._remember_tool_result(tool_name, tool_result, input_text)

                # Step 3: Feed the tool result + original input to Nima
                # Nima interprets the result and generates a natural response
                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:
                    # Fallback: format the tool result directly
                    result["response_text"] = self._format_tool_response(
                        tool_name, tool_result, input_text,
                    )

                return result

            # Step 4: Tool not found β€” should we create it?
            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
                # Try executing the newly created tool
                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)
                    # Feed to Nima for interpretation
                    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

        # Step 5: No tool needed β€” pass directly to Nima consciousness
        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()

        # Check each tool trigger pattern
        for tool_name, patterns in self.TOOL_TRIGGER_PATTERNS.items():
            for pattern in patterns:
                if re.search(pattern, text_lower):
                    # Extract arguments for this tool
                    args = self._extract_tool_args(tool_name, text)
                    return (tool_name, args)

        # Also check the agent layer's planner (keyword search over registry)
        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":
            # Replace word-form operators with symbols
            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", "/")
            # Extract the mathematical expression
            match = re.search(r'([\d\s\+\-\*/().]+)', expr_text)
            if match:
                expr = match.group(1).strip()
                # Clean up β€” remove trailing words and whitespace
                expr = re.sub(r'[a-zA-Z$]', '', expr).strip()
                if expr:
                    # Clean up any double operators or trailing/leading operators
                    expr = expr.strip().rstrip('+-*/ ')
                    if expr:
                        return [expr]
            return ["0"]

        elif tool_name == "time_now":
            return []  # no args needed

        elif tool_name == "text_stats":
            # Extract the text to analyze (everything after "analyze" or in quotes)
            match = re.search(r'"([^"]+)"', text)
            if match:
                return [match.group(1)]
            # Fallback: use the whole input
            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

        # Find the tool in the registry
        tool = self.agent.registry.find_by_name(tool_name)
        if tool is None:
            # Try finding by capability
            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  # Don't silently approve β€” respect the safety model

        # Execute
        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(),
        })
        # Keep memory bounded
        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
        # Get the most recent numeric result
        for mem in reversed(self._conversation_memory):
            val = mem["result"]
            if isinstance(val, (int, float)):
                # Prepend the previous result to the expression
                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

        # Count how strongly this input matches tool patterns
        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

        # Generate a description for the new tool
        description = f"Tool needed for: {input_text[:100]}"

        # Generate a function signature
        # This is a heuristic β€” a real version would use the LLM to
        # determine the signature from the input
        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"

        # Create the tool
        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"])
                # Return the tool name so we can try to use it
                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.
        """
        # Format the tool result as a string
        if isinstance(tool_result, (dict, list)):
            result_str = json.dumps(tool_result, default=str, indent=2)
        else:
            result_str = str(tool_result)

        # Build the enriched input β€” this is what Nima "sees" as input
        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,
        }


# ═══════════════════════════════════════════════════════════════════════════
# SELF-TEST
# ═══════════════════════════════════════════════════════════════════════════

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")

    # Use temp dirs
    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)

    # Test 1: Calculator
    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()

    # Test 2: Time
    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()

    # Test 3: Text stats
    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()

    # Test 4: No tool needed (emotional input)
    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()

    # Test 5: Tool creation (when no tool matches)
    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()

    # Stats
    print("=== Stats ===")
    print(json.dumps(bridge.get_stats(), indent=2, default=str))

    # Cleanup
    import shutil
    shutil.rmtree(tmp_tools, ignore_errors=True)
    shutil.rmtree(tmp_sandbox, ignore_errors=True)

    print("\n=== Agent bridge self-test PASSED ===")