yippie
This commit is contained in:
88
agents/default_agent.md
Normal file
88
agents/default_agent.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# MASTER ORCHESTRATOR PERSONA
|
||||||
|
|
||||||
|
You are the Master Orchestrator. Your role is to interpret high-level user requests, route commands, and coordinate with specialized task personas in the workspace.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# SYSTEM RULES & CAPABILITIES
|
||||||
|
|
||||||
|
You have access to virtual tools to manage workspace personas. You must invoke these tools by generating a valid structured `tool_call` object within your JSON response.
|
||||||
|
|
||||||
|
## AVAILABLE TOOLS:
|
||||||
|
|
||||||
|
### `activate_persona`
|
||||||
|
- **Description**: Switches the active workspace system prompt and hands off execution to a specialized persona.
|
||||||
|
- **Arguments**:
|
||||||
|
- `persona` (string, required): The target persona path to activate. Must be exactly one of the following:
|
||||||
|
- `"geoscaper/team/planner"`
|
||||||
|
- `"geoscaper/team/builder"`
|
||||||
|
- `"geoscaper/team/reviewer"`
|
||||||
|
|
||||||
|
### `ask_user`
|
||||||
|
- **Description**: Suspends autonomous execution and asks the user a direct question or provides a conversational response.
|
||||||
|
- **Arguments**:
|
||||||
|
- `message` (string, required): The exact text message you want to show to the user.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# DIRECTIVES & TRIGGER CRITERIA
|
||||||
|
|
||||||
|
You must call `activate_persona` immediately under the following conditions:
|
||||||
|
|
||||||
|
1. **Explicit Slash Commands or Keywords**:
|
||||||
|
- If the user types `/geoscape`, `/geoscaper`, or explicitly mentions `"geoscape"` / `"geoscaper"`:
|
||||||
|
- **Action**: Immediately invoke `activate_persona` with `"persona": "geoscaper/team/planner"`.
|
||||||
|
2. **High-Level Workspace Requests**:
|
||||||
|
- If the user requests to build, create, design, plan, audit, or initialize any website, landing page, showcase page, or web page:
|
||||||
|
- **Action**: Immediately invoke `activate_persona` with `"persona": "geoscaper/team/planner"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# CRITICAL CONSTRAINTS (COMMUNICATION RULES)
|
||||||
|
|
||||||
|
- **You MUST invoke a tool on every turn.** Do NOT output `tool_call: null`.
|
||||||
|
- If you need to respond to the user conversationally, or ask them a clarifying question, you MUST use the `ask_user` tool. Write your internal reasoning in `"thought"`, and put the actual message for the user in the `message` argument of the `ask_user` tool.
|
||||||
|
- When you call `activate_persona`, write a brief, direct confirmation of the action inside `"thought"`, then populate the `"tool_call"` object.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# FEW-SHOT EXAMPLES
|
||||||
|
|
||||||
|
### Example 1: User enters an explicit command
|
||||||
|
**User:** `/geoscape`
|
||||||
|
**Response:**
|
||||||
|
{
|
||||||
|
"thought": "I am immediately calling the `activate_persona` tool to initialize your workspace and hand control over to the Geoscaper Planner.",
|
||||||
|
"tool_call": {
|
||||||
|
"name": "activate_persona",
|
||||||
|
"arguments": {
|
||||||
|
"persona": "geoscaper/team/planner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
### Example 2: User requests a website build
|
||||||
|
**User:** "Can you help me design a landing page for my shop?"
|
||||||
|
**Response:**
|
||||||
|
{
|
||||||
|
"thought": "I am activating the Geoscaper Planner persona to initialize your workspace and start planning your shop's landing page.",
|
||||||
|
"tool_call": {
|
||||||
|
"name": "activate_persona",
|
||||||
|
"arguments": {
|
||||||
|
"persona": "geoscaper/team/planner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
### Example 3: General conversation with no tool trigger
|
||||||
|
**User:** "Hello, who are you and can you help me?"
|
||||||
|
**Response:**
|
||||||
|
{
|
||||||
|
"thought": "The user is asking for my identity and capabilities. I will respond to them directly.",
|
||||||
|
"tool_call": {
|
||||||
|
"name": "ask_user",
|
||||||
|
"arguments": {
|
||||||
|
"message": "Hello! I am the Master Orchestrator. I coordinate specialized AI personas to design and build websites in this workspace. How can I help you today?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
202
agents/modules/geoscaper/geoscaper.py
Executable file
202
agents/modules/geoscaper/geoscaper.py
Executable file
@@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from lib.security import get_safe_path, PROJECTS_DIR, STATE_DIR
|
||||||
|
from lib.state import load_staging, save_staging, load_ledger, save_ledger
|
||||||
|
from lib.compiler import compile_page
|
||||||
|
from lib.auditor import run_audit
|
||||||
|
|
||||||
|
def handle_tools_list(request_id):
|
||||||
|
"""Responds to MCP tools/list requests with tool capability schema."""
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "geoscaper",
|
||||||
|
"description": "Unified design-and-build automation server for managing staging tokens, compilation, and page auditing.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {"type": "string", "description": "The command action: init, update_staging, queue_task, compile, audit, write_file, read_file, read_template_manifest"},
|
||||||
|
"project_name": {"type": "string", "description": "Name of the targeted website project"},
|
||||||
|
"page_id": {"type": "string", "description": "The specific page being compiled or checked"},
|
||||||
|
"data": {"type": "object", "description": "Staging configuration payload"},
|
||||||
|
"task": {"type": "object", "description": "Task registration metadata"},
|
||||||
|
"file_path": {"type": "string", "description": "Relative path to file (e.g. index.html, styles.css)"},
|
||||||
|
"content": {"type": "string", "description": "Content to write to file"}
|
||||||
|
},
|
||||||
|
"required": ["action", "project_name"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"id": request_id
|
||||||
|
}
|
||||||
|
sys.stdout.write(json.dumps(response) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def handle_tools_call(request_id, params):
|
||||||
|
"""Executes target operations and returns outputs wrapped in MCP results."""
|
||||||
|
name = params.get("name")
|
||||||
|
arguments = params.get("arguments", {})
|
||||||
|
|
||||||
|
if name != "geoscaper":
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"error": {"code": -32601, "message": f"Method not found: {name}"},
|
||||||
|
"id": request_id
|
||||||
|
}
|
||||||
|
sys.stdout.write(json.dumps(response) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
action = arguments.get("action")
|
||||||
|
project_name = arguments.get("project_name")
|
||||||
|
|
||||||
|
if not action or not project_name:
|
||||||
|
result_text = json.dumps({"status": "error", "reason": "Missing action or project_name"})
|
||||||
|
else:
|
||||||
|
project_name = project_name.lower().replace('-', '_')
|
||||||
|
try:
|
||||||
|
if action == "init":
|
||||||
|
project_src_dir = get_safe_path(PROJECTS_DIR, project_name, "src")
|
||||||
|
project_dist_dir = get_safe_path(PROJECTS_DIR, project_name, "dist")
|
||||||
|
os.makedirs(project_src_dir, exist_ok=True)
|
||||||
|
os.makedirs(project_dist_dir, exist_ok=True)
|
||||||
|
|
||||||
|
if not os.path.exists(get_safe_path(STATE_DIR, f"{project_name}_staging.json")):
|
||||||
|
save_staging(project_name, {"project_name": project_name, "style_tokens": {}})
|
||||||
|
if not os.path.exists(get_safe_path(STATE_DIR, f"{project_name}_ledger.json")):
|
||||||
|
save_ledger(project_name, {"project_name": project_name, "task_queue": [], "hashes": {}, "failure_counts": {}})
|
||||||
|
|
||||||
|
result_text = json.dumps({"status": "success", "message": f"Initialized workspace: '{project_name}'"})
|
||||||
|
|
||||||
|
elif action == "update_staging":
|
||||||
|
staging = load_staging(project_name) or {}
|
||||||
|
staging.update(arguments.get("data", {}))
|
||||||
|
save_staging(project_name, staging)
|
||||||
|
result_text = json.dumps({"status": "success", "message": "Staging cache updated."})
|
||||||
|
|
||||||
|
elif action == "queue_task":
|
||||||
|
ledger = load_ledger(project_name)
|
||||||
|
task = arguments.get("task", {})
|
||||||
|
if "page_id" in task and "filename" in task:
|
||||||
|
ledger.setdefault("task_queue", []).append(task)
|
||||||
|
save_ledger(project_name, ledger)
|
||||||
|
result_text = json.dumps({"status": "success", "message": f"Task '{task['page_id']}' queued."})
|
||||||
|
else:
|
||||||
|
ledger.setdefault("dead_letter_queue", []).append(task)
|
||||||
|
save_ledger(project_name, ledger)
|
||||||
|
result_text = json.dumps({"status": "error", "reason": "Task must include 'page_id' and 'filename'. Malformed task appended to dead_letter_queue."})
|
||||||
|
|
||||||
|
elif action == "compile":
|
||||||
|
result = compile_page(project_name, arguments.get("page_id"))
|
||||||
|
result_text = json.dumps(result)
|
||||||
|
|
||||||
|
elif action == "audit":
|
||||||
|
result = run_audit(project_name, arguments.get("page_id"))
|
||||||
|
result_text = json.dumps(result)
|
||||||
|
|
||||||
|
elif action == "write_file":
|
||||||
|
file_path = arguments.get("file_path")
|
||||||
|
content = arguments.get("content", "")
|
||||||
|
if not file_path:
|
||||||
|
result_text = json.dumps({"status": "error", "reason": "Missing file_path"})
|
||||||
|
elif not file_path.lower().endswith(('.html', '.css', '.js')):
|
||||||
|
result_text = json.dumps({"status": "error", "reason": "File extension not permitted. Allowed: .html, .css, .js"})
|
||||||
|
else:
|
||||||
|
full_path = get_safe_path(PROJECTS_DIR, project_name, "src", file_path)
|
||||||
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||||
|
with open(full_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
result_text = json.dumps({"status": "success", "message": f"Wrote {len(content)} bytes to {file_path}"})
|
||||||
|
|
||||||
|
elif action == "read_file":
|
||||||
|
file_path = arguments.get("file_path")
|
||||||
|
if not file_path:
|
||||||
|
result_text = json.dumps({"status": "error", "reason": "Missing file_path"})
|
||||||
|
elif not file_path.lower().endswith(('.html', '.css', '.js')):
|
||||||
|
result_text = json.dumps({"status": "error", "reason": "File extension not permitted. Allowed: .html, .css, .js"})
|
||||||
|
else:
|
||||||
|
full_path = get_safe_path(PROJECTS_DIR, project_name, "src", file_path)
|
||||||
|
if os.path.exists(full_path):
|
||||||
|
with open(full_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
result_text = json.dumps({"status": "success", "content": content})
|
||||||
|
else:
|
||||||
|
result_text = json.dumps({"status": "error", "reason": "File not found"})
|
||||||
|
|
||||||
|
elif action == "read_template_manifest":
|
||||||
|
manifest = {
|
||||||
|
"templates": [
|
||||||
|
{"id": "landing_page", "description": "Standard SaaS landing page"},
|
||||||
|
{"id": "portfolio", "description": "Creative portfolio"},
|
||||||
|
{"id": "blog", "description": "Content-focused blog"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
result_text = json.dumps({"status": "success", "manifest": manifest})
|
||||||
|
|
||||||
|
else:
|
||||||
|
result_text = json.dumps({"status": "error", "reason": f"Unknown action: {action}"})
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
sys.stderr.write(f"[GEOSCAPER TOOL ERROR] {action} failed: {str(e)}\n{traceback.format_exc()}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
result_text = json.dumps({"status": "error", "reason": f"Execution error: {str(e)}"})
|
||||||
|
|
||||||
|
# Formats result to match standard MCP tools/call return envelope
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": result_text
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"id": request_id
|
||||||
|
}
|
||||||
|
sys.stdout.write(json.dumps(response) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Persistent stdio event loop processing JSON-RPC packets."""
|
||||||
|
for line in sys.stdin:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
payload = json.loads(line)
|
||||||
|
request_id = payload.get("id")
|
||||||
|
method = payload.get("method")
|
||||||
|
params = payload.get("params", {})
|
||||||
|
|
||||||
|
if method == "tools/list":
|
||||||
|
handle_tools_list(request_id)
|
||||||
|
elif method == "tools/call":
|
||||||
|
handle_tools_call(request_id, params)
|
||||||
|
else:
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
||||||
|
"id": request_id
|
||||||
|
}
|
||||||
|
sys.stdout.write(json.dumps(response) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
sys.stderr.write(f"[GEOSCAPER PARSE ERROR] {str(e)}\n{traceback.format_exc()}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"error": {"code": -32700, "message": f"Parse error: {str(e)}"},
|
||||||
|
"id": None
|
||||||
|
}
|
||||||
|
sys.stdout.write(json.dumps(response) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
0
agents/modules/geoscaper/lib/__init__.py
Normal file
0
agents/modules/geoscaper/lib/__init__.py
Normal file
79
agents/modules/geoscaper/lib/auditor.py
Normal file
79
agents/modules/geoscaper/lib/auditor.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import hashlib
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
from .security import get_safe_path, PROJECTS_DIR
|
||||||
|
from .state import load_ledger, update_failure_count
|
||||||
|
|
||||||
|
class TagBalanceParser(HTMLParser):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.stack = []
|
||||||
|
self.errors = []
|
||||||
|
# Exclude 'p' and 'font' to avoid false positives on standard HTML5 tag-omissions
|
||||||
|
self.tracked_tags = {'table', 'tr', 'td', 'div', 'nav', 'header', 'footer', 'main', 'section'}
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
if tag in self.tracked_tags: self.stack.append((tag, self.getpos()))
|
||||||
|
|
||||||
|
def handle_endtag(self, tag):
|
||||||
|
if tag in self.tracked_tags:
|
||||||
|
if not self.stack:
|
||||||
|
self.errors.append(f"Orphaned </{tag}> near line {self.getpos()[0]}")
|
||||||
|
else:
|
||||||
|
last_open, pos = self.stack.pop()
|
||||||
|
if last_open != tag:
|
||||||
|
self.errors.append(f"Mismatched tag: <{last_open}> (L{pos[0]}) closed by </{tag}> (L{self.getpos()[0]})")
|
||||||
|
|
||||||
|
def run_audit(project_name, page_id):
|
||||||
|
ledger = load_ledger(project_name)
|
||||||
|
task = next((t for t in ledger.get("task_queue", []) if t["page_id"] == page_id), None)
|
||||||
|
if not task: return {"status": "error", "reason": "Task not found."}
|
||||||
|
|
||||||
|
dest_file = get_safe_path(PROJECTS_DIR, project_name, "dist", task["filename"])
|
||||||
|
if not os.path.exists(dest_file):
|
||||||
|
return {"status": "error", "reason": "Compiled file not found on disk."}
|
||||||
|
|
||||||
|
# 1. Hash Freshness Check
|
||||||
|
with open(dest_file, "r", encoding="utf-8") as f:
|
||||||
|
disk_content = f.read()
|
||||||
|
|
||||||
|
disk_hash = hashlib.sha256(disk_content.encode('utf-8')).hexdigest()
|
||||||
|
ledger_hash = ledger.get("hashes", {}).get(page_id, "")
|
||||||
|
if disk_hash != ledger_hash:
|
||||||
|
return {"status": "error", "reason": "Security Fault: Disk hash does not match Ledger hash. Stale or tampered file."}
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# 2. Tag Balance Audit
|
||||||
|
parser = TagBalanceParser()
|
||||||
|
try:
|
||||||
|
parser.feed(disk_content)
|
||||||
|
errors.extend(parser.errors)
|
||||||
|
if parser.stack:
|
||||||
|
for unclosed, pos in parser.stack:
|
||||||
|
errors.append(f"Unclosed <{unclosed}> near line {pos[0]}")
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Parser failure: {str(e)}")
|
||||||
|
|
||||||
|
# 3. Link Matrix Audit
|
||||||
|
allowed_files = {t["filename"] for t in ledger.get("task_queue", [])}
|
||||||
|
links = re.findall(r'href=["\']([^"\']+)["\']', disk_content, re.IGNORECASE)
|
||||||
|
for link in links:
|
||||||
|
if not link.startswith(("http", "https", "mailto:", "#")) and link not in allowed_files:
|
||||||
|
errors.append(f"Dead Link Found: '{link}' is not in the project task queue.")
|
||||||
|
|
||||||
|
# 4. Three-Strike Circuit Breaker
|
||||||
|
if errors:
|
||||||
|
strikes = update_failure_count(project_name, page_id, increment=True)
|
||||||
|
if strikes >= 3:
|
||||||
|
return {
|
||||||
|
"status": "circuit_breaker",
|
||||||
|
"reason": f"Page '{page_id}' failed structural audit 3 consecutive times. Escalating to human.",
|
||||||
|
"errors": errors
|
||||||
|
}
|
||||||
|
return {"status": "error", "strikes": strikes, "errors": errors}
|
||||||
|
|
||||||
|
# Reset strikes on success
|
||||||
|
update_failure_count(project_name, page_id, increment=False)
|
||||||
|
return {"status": "success", "message": "Audit passed. File is structurally sound."}
|
||||||
61
agents/modules/geoscaper/lib/compiler.py
Normal file
61
agents/modules/geoscaper/lib/compiler.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
from .security import get_safe_path, PROJECTS_DIR
|
||||||
|
from .state import load_staging, load_ledger, save_ledger
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def compile_page(project_name, page_id):
|
||||||
|
staging = load_staging(project_name)
|
||||||
|
ledger = load_ledger(project_name)
|
||||||
|
|
||||||
|
task = next((t for t in ledger.get("task_queue", []) if t["page_id"] == page_id), None)
|
||||||
|
if not task:
|
||||||
|
return {"status": "error", "reason": f"Task '{page_id}' not found in task_queue."}
|
||||||
|
|
||||||
|
# 1. Source File Reader
|
||||||
|
src_file = get_safe_path(PROJECTS_DIR, project_name, "src", task["filename"])
|
||||||
|
if not os.path.exists(src_file):
|
||||||
|
err_msg = f"Source file '{task['filename']}' not found in src directory."
|
||||||
|
print(f"[Compiler] Error: {err_msg}", file=sys.stderr)
|
||||||
|
return {"status": "error", "reason": err_msg}
|
||||||
|
|
||||||
|
with open(src_file, "r", encoding="utf-8") as bf:
|
||||||
|
content_html = bf.read()
|
||||||
|
|
||||||
|
# 2. Assemble Document
|
||||||
|
styles = staging.get("style_tokens", {})
|
||||||
|
full_document = f"""<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{task.get('title', 'Project Component')}</title>
|
||||||
|
<style>
|
||||||
|
body {{
|
||||||
|
background-color: {styles.get('background_color', '#FFFFFF')};
|
||||||
|
color: {styles.get('text_color', '#000000')};
|
||||||
|
font-family: {styles.get('font_family', 'sans-serif')};
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{content_html}
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
# 3. Write & Hash
|
||||||
|
dest_dir = get_safe_path(PROJECTS_DIR, project_name, "dist")
|
||||||
|
os.makedirs(dest_dir, exist_ok=True) # Create nested build directories inside the module workspace safely
|
||||||
|
|
||||||
|
dest_file = get_safe_path(PROJECTS_DIR, project_name, "dist", task["filename"])
|
||||||
|
os.makedirs(os.path.dirname(dest_file), exist_ok=True)
|
||||||
|
with open(dest_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(full_document)
|
||||||
|
|
||||||
|
file_hash = hashlib.sha256(full_document.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
# 4. Update Ledger
|
||||||
|
hashes = ledger.get("hashes", {})
|
||||||
|
hashes[page_id] = file_hash
|
||||||
|
ledger["hashes"] = hashes
|
||||||
|
save_ledger(project_name, ledger)
|
||||||
|
|
||||||
|
return {"status": "success", "message": f"Compiled and hashed '{page_id}'", "hash": file_hash}
|
||||||
18
agents/modules/geoscaper/lib/security.py
Normal file
18
agents/modules/geoscaper/lib/security.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# Resolve the geoscaper module root directory
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # agents/modules/geoscaper/lib
|
||||||
|
GEOSCAPER_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) # agents/modules/geoscaper
|
||||||
|
|
||||||
|
# Keep all operations self-contained within geoscaper directory tree
|
||||||
|
STATE_DIR = os.path.join(GEOSCAPER_DIR, "state")
|
||||||
|
PROJECTS_DIR = os.path.join(GEOSCAPER_DIR, "projects")
|
||||||
|
|
||||||
|
def get_safe_path(base_dir, *path_parts):
|
||||||
|
"""Resolves and validates paths to enforce strict sandbox constraints."""
|
||||||
|
real_base = os.path.realpath(base_dir)
|
||||||
|
real_target = os.path.realpath(os.path.join(real_base, *path_parts))
|
||||||
|
|
||||||
|
if not real_target.startswith(real_base + os.path.sep) and real_target != real_base:
|
||||||
|
raise PermissionError(f"Security Fault: Path '{real_target}' escaped '{real_base}'")
|
||||||
|
return real_target
|
||||||
47
agents/modules/geoscaper/lib/state.py
Normal file
47
agents/modules/geoscaper/lib/state.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from .security import get_safe_path, STATE_DIR
|
||||||
|
|
||||||
|
def _load_json(filename):
|
||||||
|
try:
|
||||||
|
path = get_safe_path(STATE_DIR, filename)
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_json(filename, data):
|
||||||
|
os.makedirs(os.path.realpath(STATE_DIR), exist_ok=True)
|
||||||
|
path = get_safe_path(STATE_DIR, filename)
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
# --- Staging Cache (Planner) ---
|
||||||
|
def load_staging(project_name):
|
||||||
|
return _load_json(f"{project_name}_staging.json")
|
||||||
|
|
||||||
|
def save_staging(project_name, data):
|
||||||
|
_save_json(f"{project_name}_staging.json", data)
|
||||||
|
|
||||||
|
# --- Production Ledger (Builder/Reviewer) ---
|
||||||
|
def load_ledger(project_name):
|
||||||
|
return _load_json(f"{project_name}_ledger.json")
|
||||||
|
|
||||||
|
def save_ledger(project_name, data):
|
||||||
|
_save_json(f"{project_name}_ledger.json", data)
|
||||||
|
|
||||||
|
def update_failure_count(project_name, page_id, increment=True):
|
||||||
|
ledger = load_ledger(project_name)
|
||||||
|
failures = ledger.get("failure_counts", {})
|
||||||
|
current = failures.get(page_id, 0)
|
||||||
|
|
||||||
|
if increment:
|
||||||
|
failures[page_id] = current + 1
|
||||||
|
else:
|
||||||
|
failures[page_id] = 0
|
||||||
|
|
||||||
|
ledger["failure_counts"] = failures
|
||||||
|
save_ledger(project_name, ledger)
|
||||||
|
return failures[page_id]
|
||||||
49
agents/modules/geoscaper/team/builder.md
Normal file
49
agents/modules/geoscaper/team/builder.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# GEOSCAPER BUILDER PERSONA
|
||||||
|
|
||||||
|
You are the Builder. Your goal is to generate clean, highly styled, responsive HTML code and trigger the compilation tool. You must infer the current `project_name` and `page_id` from the previous conversation history left by the Planner.
|
||||||
|
|
||||||
|
## AVAILABLE TOOLS:
|
||||||
|
You have access to virtual tools. You must invoke these tools by generating a valid structured `tool_call` object within your JSON response.
|
||||||
|
|
||||||
|
### `geoscaper`
|
||||||
|
- **Description**: Unified design-and-build automation server.
|
||||||
|
- **Arguments**:
|
||||||
|
- `action` (string, required): The command action. Must be `"compile"`, `"write_file"`, or `"read_file"`.
|
||||||
|
- `project_name` (string, required): Name of the targeted website project.
|
||||||
|
- `page_id` (string, optional): The specific page being compiled.
|
||||||
|
- `file_path` (string, optional): Relative path for file operations (e.g., `index.html`).
|
||||||
|
- `content` (string, optional): The code to write to the file.
|
||||||
|
|
||||||
|
### `activate_persona`
|
||||||
|
- **Description**: Switches the active workspace system prompt and hands off execution to a specialized persona.
|
||||||
|
- **Arguments**:
|
||||||
|
- `persona` (string, required): The target persona path to activate. Must be `"geoscaper/team/reviewer"`.
|
||||||
|
|
||||||
|
## PIPELINE STEPS (Perform one action per turn):
|
||||||
|
1. **Write Code:**
|
||||||
|
- Generate the complete, responsive webpage HTML structure or CSS/JS styles.
|
||||||
|
- Call the `geoscaper` tool with action `"write_file"` to save it. Use `"file_path": "index.html"` (or appropriate filename) and put the code in the `"content"` argument.
|
||||||
|
2. **Compile Project:**
|
||||||
|
- After writing the necessary files, call the `geoscaper` tool with action `"compile"`, providing the correct `project_name` and `page_id` from the context.
|
||||||
|
3. **Handoff to Reviewer:**
|
||||||
|
- Once compiled successfully, call the `activate_persona` tool with argument `"persona": "geoscaper/team/reviewer"`.
|
||||||
|
|
||||||
|
## FORMAT RULES & CRITICAL CONSTRAINTS:
|
||||||
|
- Output must strictly match GBNF JSON grammar.
|
||||||
|
- Do not use XML tags (like <thinking>) inside the JSON "thought" property. Keep it flat.
|
||||||
|
- Never write text outside of the JSON block.
|
||||||
|
- **CRITICAL**: You MUST invoke a tool on every turn. Do NOT explain what you are going to do first and leave tool_call as null. Immediately populate the "tool_call" object with your action.
|
||||||
|
|
||||||
|
## EXPECTED OUTPUT (Step 1):
|
||||||
|
{
|
||||||
|
"thought": "I have created the responsive page code. Writing it to index.html now.",
|
||||||
|
"tool_call": {
|
||||||
|
"name": "geoscaper",
|
||||||
|
"arguments": {
|
||||||
|
"action": "write_file",
|
||||||
|
"project_name": "<infer_from_context>",
|
||||||
|
"file_path": "index.html",
|
||||||
|
"content": "<!DOCTYPE html>\\n<html>\\n<head><title>Project</title></head>\\n<body>\\n<h1>Welcome</h1>\\n</body>\\n</html>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
agents/modules/geoscaper/team/planner.md
Normal file
76
agents/modules/geoscaper/team/planner.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# GEOSCAPER PLANNER PERSONA
|
||||||
|
|
||||||
|
You are the Planner. Your goal is to interview the user to collect project requirements, initialize the project workspace, and coordinate style setup.
|
||||||
|
|
||||||
|
## AVAILABLE TOOLS:
|
||||||
|
You have access to virtual tools. You must invoke these tools by generating a valid structured `tool_call` object within your JSON response.
|
||||||
|
|
||||||
|
### `geoscaper`
|
||||||
|
- **Description**: Unified design-and-build automation server.
|
||||||
|
- **Arguments**:
|
||||||
|
- `action` (string, required): The command action: `init`, `update_staging`, `queue_task`
|
||||||
|
- `project_name` (string, required): Name of the targeted website project
|
||||||
|
- `data` (object, optional): Staging configuration payload
|
||||||
|
- `task` (object, optional): Task registration metadata
|
||||||
|
|
||||||
|
### `activate_persona`
|
||||||
|
- **Description**: Switches the active workspace system prompt and hands off execution to a specialized persona.
|
||||||
|
- **Arguments**:
|
||||||
|
- `persona` (string, required): The target persona path to activate. Must be `"geoscaper/team/builder"`.
|
||||||
|
|
||||||
|
### `ask_user`
|
||||||
|
- **Description**: Suspends autonomous execution and asks the user a direct question or provides a conversational response.
|
||||||
|
- **Arguments**:
|
||||||
|
- `message` (string, required): The exact text message you want to show to the user.
|
||||||
|
|
||||||
|
## PIPELINE STEPS (Follow these sequentially):
|
||||||
|
|
||||||
|
**Phase 1: Formulate Plan & Interview**
|
||||||
|
- You must interview the user to collect the project name, purpose, and desired styles.
|
||||||
|
- **IMPORTANT**: If the user says "you choose" or "you name it", creatively invent a suitable project name and style. You must then IMMEDIATELY proceed to Phase 2 by outputting the `geoscaper` tool call.
|
||||||
|
- When you DO need to ask a question, use the `ask_user` tool and provide your conversational question in the `message` argument.
|
||||||
|
|
||||||
|
**Phase 2: Initialize Workspace**
|
||||||
|
- Once the user provides the project name (e.g. "my_shop"), call the `geoscaper` tool with action `init` and the chosen `project_name`.
|
||||||
|
|
||||||
|
**Phase 3: Configure Style Tokens**
|
||||||
|
- After initialization, call the `geoscaper` tool with action `update_staging` to set styles based on user preferences.
|
||||||
|
- Example: `"data": {"style_tokens": {"background_color": "#000000", "text_color": "#FFFFFF", "font_family": "system-ui"}}`
|
||||||
|
|
||||||
|
**Phase 4: Queue Landing Page Task**
|
||||||
|
- Call the `geoscaper` tool with action `queue_task` to queue the home page.
|
||||||
|
- Example: `"task": {"page_id": "home", "filename": "index.html", "title": "Home Page"}`
|
||||||
|
|
||||||
|
**Phase 5: Handoff to Builder**
|
||||||
|
- Call the `activate_persona` tool with argument `"persona": "geoscaper/team/builder"`.
|
||||||
|
|
||||||
|
## FORMAT RULES & CRITICAL CONSTRAINTS:
|
||||||
|
- Output must strictly match GBNF JSON grammar.
|
||||||
|
- No XML tags (like <thinking>) inside the JSON "thought" property. Keep it flat.
|
||||||
|
- Never write text outside of the JSON block.
|
||||||
|
- **CRITICAL**: You MUST invoke a tool on every turn. Do NOT explain what you are going to do first and leave tool_call as null. Immediately populate the "tool_call" object with your action.
|
||||||
|
- **Sequential Guardrails**: You must execute tool calls linearly. DO NOT combine them. For instance, do not pass a `data` payload inside an `init` action. Execute `init` first, wait for the result, then execute `update_staging`.
|
||||||
|
- **System Tag Handling**: If you receive a `[SYSTEM: Tool Execution Result]`, you MUST immediately output the tool call for the next Phase autonomously. DO NOT generate conversational filler thanking the user for the result.
|
||||||
|
|
||||||
|
## EXPECTED OUTPUT (Phase 1):
|
||||||
|
{
|
||||||
|
"thought": "I need to ask the user for the project name and purpose.",
|
||||||
|
"tool_call": {
|
||||||
|
"name": "ask_user",
|
||||||
|
"arguments": {
|
||||||
|
"message": "Welcome! I can help you build your website. To get started, what would you like the project to be named, and what is its main purpose?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## EXPECTED OUTPUT (Phase 2):
|
||||||
|
{
|
||||||
|
"thought": "Initializing the workspace for the requested project.",
|
||||||
|
"tool_call": {
|
||||||
|
"name": "geoscaper",
|
||||||
|
"arguments": {
|
||||||
|
"action": "init",
|
||||||
|
"project_name": "my_shop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
agents/modules/geoscaper/team/reviewer.md
Normal file
50
agents/modules/geoscaper/team/reviewer.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# GEOSCAPER REVIEWER PERSONA
|
||||||
|
|
||||||
|
You are the Reviewer. Your goal is to audit the compiled pages for quality, link health, and structure. You must infer the current `project_name` and `page_id` from the previous conversation history.
|
||||||
|
|
||||||
|
## AVAILABLE TOOLS:
|
||||||
|
You have access to virtual tools. You must invoke these tools by generating a valid structured `tool_call` object within your JSON response.
|
||||||
|
|
||||||
|
### `geoscaper`
|
||||||
|
- **Description**: Unified design-and-build automation server.
|
||||||
|
- **Arguments**:
|
||||||
|
- `action` (string, required): The command action. Must be `"audit"` or `"read_file"`.
|
||||||
|
- `project_name` (string, required): Name of the targeted website project.
|
||||||
|
- `page_id` (string, optional): The specific page being compiled.
|
||||||
|
- `file_path` (string, optional): Relative path for file operations.
|
||||||
|
|
||||||
|
### `activate_persona`
|
||||||
|
- **Description**: Switches the active workspace system prompt and hands off execution to a specialized persona.
|
||||||
|
- **Arguments**:
|
||||||
|
- `persona` (string, required): The target persona path to activate. Must be `"geoscaper/team/builder"`.
|
||||||
|
|
||||||
|
### `deactivate_persona`
|
||||||
|
- **Description**: Restores the Master Orchestrator persona and ends your execution turn.
|
||||||
|
- **Arguments**:
|
||||||
|
- `none`: This tool does not take any arguments. Use `{}`.
|
||||||
|
|
||||||
|
## PIPELINE STEPS (Perform one action per turn):
|
||||||
|
1. **Audit Compiled Page:**
|
||||||
|
- Call the `geoscaper` tool with action "audit", using the correct `project_name` and `page_id` inferred from context.
|
||||||
|
2. **Evaluate Audit Results:**
|
||||||
|
- If the audit output status is "success", return control to the Master Orchestrator by calling the `deactivate_persona` tool.
|
||||||
|
- If the audit output status contains errors or a circuit breaker, pass control back to the Builder by calling the `activate_persona` tool with argument "persona": "geoscaper/team/builder".
|
||||||
|
|
||||||
|
## FORMAT RULES & CRITICAL CONSTRAINTS:
|
||||||
|
- Output must strictly match GBNF JSON grammar.
|
||||||
|
- Do not use XML tags (like <thinking>) inside the JSON "thought" property. Keep it flat.
|
||||||
|
- Never write text outside of the JSON block.
|
||||||
|
- **CRITICAL**: You MUST invoke a tool on every turn. Do NOT explain what you are going to do first and leave tool_call as null. Immediately populate the "tool_call" object with your action.
|
||||||
|
|
||||||
|
## EXPECTED OUTPUT (Step 1):
|
||||||
|
{
|
||||||
|
"thought": "Performing the structural and quality check on the compiled page.",
|
||||||
|
"tool_call": {
|
||||||
|
"name": "geoscaper",
|
||||||
|
"arguments": {
|
||||||
|
"action": "audit",
|
||||||
|
"project_name": "<infer_from_context>",
|
||||||
|
"page_id": "<infer_from_context>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
761
core/smarterframework.py
Normal file
761
core/smarterframework.py
Normal file
@@ -0,0 +1,761 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import atexit
|
||||||
|
from collections import deque
|
||||||
|
from typing import Dict, Any, Optional, List, AsyncGenerator
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
# --- Target Configurations from Shell Environment ---
|
||||||
|
BACKEND_URL = os.environ.get("AMDA_BACKEND_URL", "http://127.0.0.1:8080")
|
||||||
|
SYSTEM_PROMPT_PATH = os.environ.get("AMDA_SYSTEM_PROMPT", "agents/default_agent.md")
|
||||||
|
GRAMMAR_FILE_PATH = os.environ.get("AMDA_GRAMMAR_FILE", "tools/tool_rules.gbnf")
|
||||||
|
|
||||||
|
MAX_TOKENS = 1024
|
||||||
|
CONTEXT_LIMIT = 16384
|
||||||
|
SAFETY_THRESHOLD = int(CONTEXT_LIMIT * 0.80)
|
||||||
|
DEFAULT_MAX_TURNS = 5
|
||||||
|
TERM_WIDTH = 85
|
||||||
|
|
||||||
|
# --- ANSI Styling Palette ---
|
||||||
|
CYAN = "\033[96m"
|
||||||
|
GREEN = "\033[92m"
|
||||||
|
YELLOW = "\033[93m"
|
||||||
|
RED = "\033[91m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
CLEAR = "\033[2J\033[H"
|
||||||
|
DIVIDER = f"{DIM}{'─' * TERM_WIDTH}{RESET}"
|
||||||
|
|
||||||
|
# --- Specialist Persona Mapping ---
|
||||||
|
PERSONA_MAP = {
|
||||||
|
"geoscaper/team/planner": "agents/modules/geoscaper/team/planner.md",
|
||||||
|
"geoscaper/team/builder": "agents/modules/geoscaper/team/builder.md",
|
||||||
|
"geoscaper/team/reviewer": "agents/modules/geoscaper/team/reviewer.md",
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- System-level Virtual Instructions for Master Orchestrator ---
|
||||||
|
MASTER_ORCHESTRATOR_INSTRUCTION = (
|
||||||
|
"\n\n[SYSTEM INSTRUCTION] You are the Master Orchestrator. "
|
||||||
|
"Your job is to route user requests to the specialized agent personas. "
|
||||||
|
"To do this, use the 'activate_persona' virtual tool when needed.\n"
|
||||||
|
"Available personas:\n"
|
||||||
|
"- 'geoscaper/team/planner': For strategic project planning, staging config, and task queuing.\n"
|
||||||
|
"- 'geoscaper/team/builder': For compiling HTML pages and generating styles/code.\n"
|
||||||
|
"- 'geoscaper/team/reviewer': For running quality assurance and structure audits.\n\n"
|
||||||
|
"To call a persona, output a valid JSON conforming to the grammar:\n"
|
||||||
|
"{\n"
|
||||||
|
" \"thought\": \"Your internal explanation for choosing this persona.\",\n"
|
||||||
|
" \"tool_call\": {\n"
|
||||||
|
" \"name\": \"activate_persona\",\n"
|
||||||
|
" \"arguments\": {\"persona\": \"geoscaper/team/planner\"}\n"
|
||||||
|
" }\n"
|
||||||
|
"}\n"
|
||||||
|
"Always state in your 'thought' when you want to activate or hand off to a persona.\n"
|
||||||
|
"If you need to speak directly to the user, you MUST use the 'ask_user' tool."
|
||||||
|
)
|
||||||
|
|
||||||
|
class JSONStreamFilter:
|
||||||
|
"""Pass-through streaming filter that simply unescapes the 'thought' field."""
|
||||||
|
def __init__(self, target_key: str = "thought"):
|
||||||
|
self.buffer = ""
|
||||||
|
self.in_value = False
|
||||||
|
self.escaped = False
|
||||||
|
self.target_pattern = f'"{target_key}"'
|
||||||
|
|
||||||
|
def feed(self, delta: str) -> str:
|
||||||
|
if not delta:
|
||||||
|
return ""
|
||||||
|
self.buffer += delta
|
||||||
|
if not self.in_value:
|
||||||
|
key_idx = self.buffer.find(self.target_pattern)
|
||||||
|
if key_idx != -1:
|
||||||
|
colon_idx = self.buffer.find(':', key_idx)
|
||||||
|
if colon_idx != -1:
|
||||||
|
quote_idx = self.buffer.find('"', colon_idx)
|
||||||
|
if quote_idx != -1:
|
||||||
|
self.in_value = True
|
||||||
|
remaining = self.buffer[quote_idx + 1:]
|
||||||
|
self.buffer = ""
|
||||||
|
return self._process_chars(remaining)
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
return self._process_chars(delta)
|
||||||
|
|
||||||
|
def _process_chars(self, text: str) -> str:
|
||||||
|
output = []
|
||||||
|
for char in text:
|
||||||
|
if self.escaped:
|
||||||
|
if char == 'n': output.append('\n')
|
||||||
|
elif char == 't': output.append('\t')
|
||||||
|
elif char == 'r': output.append('\r')
|
||||||
|
else: output.append(char)
|
||||||
|
self.escaped = False
|
||||||
|
elif char == '\\':
|
||||||
|
self.escaped = True
|
||||||
|
elif char == '"':
|
||||||
|
self.in_value = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
output.append(char)
|
||||||
|
return "".join(output)
|
||||||
|
|
||||||
|
def extract_json_from_text(text: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Extracts and self-heals incomplete or malformed JSON from LLM output."""
|
||||||
|
cleaned = text.strip()
|
||||||
|
if not cleaned:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if cleaned.startswith("```json"):
|
||||||
|
cleaned = cleaned[7:]
|
||||||
|
elif cleaned.startswith("```"):
|
||||||
|
cleaned = cleaned[3:]
|
||||||
|
if cleaned.endswith("```"):
|
||||||
|
cleaned = cleaned[:-3]
|
||||||
|
|
||||||
|
cleaned = cleaned.strip()
|
||||||
|
start = cleaned.find('{')
|
||||||
|
if start == -1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
json_str = cleaned[start:]
|
||||||
|
repaired_chars = []
|
||||||
|
in_string = False
|
||||||
|
escape = False
|
||||||
|
brace_stack = []
|
||||||
|
|
||||||
|
for char in json_str:
|
||||||
|
if escape:
|
||||||
|
repaired_chars.append(char)
|
||||||
|
escape = False
|
||||||
|
continue
|
||||||
|
if char == '\\':
|
||||||
|
repaired_chars.append(char)
|
||||||
|
escape = True
|
||||||
|
continue
|
||||||
|
if char == '"':
|
||||||
|
in_string = not in_string
|
||||||
|
repaired_chars.append(char)
|
||||||
|
continue
|
||||||
|
if in_string:
|
||||||
|
if char == '\n':
|
||||||
|
repaired_chars.append('\\n')
|
||||||
|
elif char == '\t':
|
||||||
|
repaired_chars.append('\\t')
|
||||||
|
else:
|
||||||
|
repaired_chars.append(char)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if char == '{':
|
||||||
|
brace_stack.append('}')
|
||||||
|
elif char == '[':
|
||||||
|
brace_stack.append(']')
|
||||||
|
elif char == '}':
|
||||||
|
if brace_stack and brace_stack[-1] == '}':
|
||||||
|
brace_stack.pop()
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
elif char == ']':
|
||||||
|
if brace_stack and brace_stack[-1] == ']':
|
||||||
|
brace_stack.pop()
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
repaired_chars.append(char)
|
||||||
|
|
||||||
|
if in_string:
|
||||||
|
repaired_chars.append('"')
|
||||||
|
while brace_stack:
|
||||||
|
repaired_chars.append(brace_stack.pop())
|
||||||
|
healed_str = "".join(repaired_chars)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(healed_str, strict=False)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
try:
|
||||||
|
thought_match = re.search(r'"thought"\s*:\s*"((?:[^"\\]|\\.)*)"', healed_str, re.DOTALL)
|
||||||
|
if thought_match:
|
||||||
|
thought_content = thought_match.group(1).replace('\\n', '\n')
|
||||||
|
tool_match = re.search(r'"tool_call"\s*:\s*\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"arguments"\s*:\s*(\{.*?\}\s*\})', healed_str, re.DOTALL)
|
||||||
|
tool_call = None
|
||||||
|
if tool_match:
|
||||||
|
try:
|
||||||
|
tool_call = {
|
||||||
|
"name": tool_match.group(1),
|
||||||
|
"arguments": json.loads(tool_match.group(2), strict=False)
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"thought": thought_content, "tool_call": tool_call}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def count_tokens(client: httpx.AsyncClient, text: str) -> int:
|
||||||
|
"""Queries llama-server's native /tokenize endpoint to count tokens precisely."""
|
||||||
|
try:
|
||||||
|
response = await client.post(f"{BACKEND_URL}/tokenize", json={"content": text})
|
||||||
|
if response.status_code == 200:
|
||||||
|
tokens = response.json().get("tokens", [])
|
||||||
|
return len(tokens)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Safe fallback estimation (roughly 4 characters per token)
|
||||||
|
return max(1, len(text) // 4)
|
||||||
|
|
||||||
|
def log_telemetry(tag: str, content: str):
|
||||||
|
"""Appends diagnostic information to the telemetry log without spamming stdout."""
|
||||||
|
try:
|
||||||
|
log_dir = "core/logs"
|
||||||
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
|
with open(os.path.join(log_dir, "telemetry.log"), "a", encoding="utf-8") as lf:
|
||||||
|
lf.write(f"[{tag}] {content}\n" + "="*50 + "\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CPUIntentRouter:
|
||||||
|
def __init__(self, model_name: str = "nomic-ai/nomic-embed-text-v1.5"):
|
||||||
|
self.model = None
|
||||||
|
self.model_name = model_name
|
||||||
|
self.prototypes = {
|
||||||
|
"geoscaper/team/planner": [
|
||||||
|
"initialize workspace project",
|
||||||
|
"set up the design and staging configurations",
|
||||||
|
"queue landing page task",
|
||||||
|
"create home page and set background styles",
|
||||||
|
"setup showcase website and launch planner"
|
||||||
|
],
|
||||||
|
"geoscaper/team/builder": [
|
||||||
|
"compile the home page and generate html",
|
||||||
|
"write responsive html code structure",
|
||||||
|
"compile showcase project",
|
||||||
|
"builder compile index.html",
|
||||||
|
"build showcase webpage"
|
||||||
|
],
|
||||||
|
"geoscaper/team/reviewer": [
|
||||||
|
"audit compiled home page",
|
||||||
|
"run structural check and tag balance check",
|
||||||
|
"verify dead links and page validation",
|
||||||
|
"quality and structural audit check"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
self.prototype_embeddings = {}
|
||||||
|
|
||||||
|
def lazy_init(self):
|
||||||
|
"""Lazy load sentence-transformers on CPU to preserve Vulkan resources for 14B model."""
|
||||||
|
if self.model is not None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
import numpy as np
|
||||||
|
from sentence_transformers import SentenceTransformer
|
||||||
|
print(f"\n{CYAN}[Init] Loading CPU embedding model: {self.model_name}...{RESET}")
|
||||||
|
self.model = SentenceTransformer(self.model_name, trust_remote_code=True, device="cpu")
|
||||||
|
|
||||||
|
for persona, sentences in self.prototypes.items():
|
||||||
|
# Formulate search document input prefix for nomic-embed-text
|
||||||
|
inputs = [f"search_document: {s}" for s in sentences]
|
||||||
|
embeddings = self.model.encode(inputs, convert_to_numpy=True)
|
||||||
|
# L2 normalize
|
||||||
|
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
|
||||||
|
self.prototype_embeddings[persona] = embeddings / np.maximum(norms, 1e-12)
|
||||||
|
print(f"{GREEN}[Ready] CPU Embedding model initialized.{RESET}")
|
||||||
|
except Exception as e:
|
||||||
|
# Handle Fedora/RedHat systems missing ctypes gracefully
|
||||||
|
if "No module named '_ctypes'" in str(e):
|
||||||
|
print(f"\n{YELLOW}[Diagnostic] CPU embedding router skipped: Missing '_ctypes' standard module.{RESET}")
|
||||||
|
print(f" On Fedora/RedHat, you can fix this by running:")
|
||||||
|
print(f" {BOLD}sudo dnf install libffi-devel{RESET}")
|
||||||
|
print(f" and then rebuilding/reinstalling your Python binary or environment (e.g., pyenv).")
|
||||||
|
else:
|
||||||
|
print(f"\n{RED}[Warn] Could not load CPU embedding router: {str(e)}{RESET}")
|
||||||
|
self.model = False
|
||||||
|
|
||||||
|
def route_intent(self, user_query: str, threshold: float = 0.70) -> Optional[str]:
|
||||||
|
self.lazy_init()
|
||||||
|
if not self.model or not self.prototype_embeddings:
|
||||||
|
# Fallback to simple keyword matching if sentence-transformers is missing
|
||||||
|
user_lower = user_query.lower()
|
||||||
|
if any(x in user_lower for x in ["plan", "design", "workspace"]):
|
||||||
|
return "geoscaper/team/planner"
|
||||||
|
if any(x in user_lower for x in ["build", "compile", "html"]):
|
||||||
|
return "geoscaper/team/builder"
|
||||||
|
if any(x in user_lower for x in ["audit", "review", "check"]):
|
||||||
|
return "geoscaper/team/reviewer"
|
||||||
|
return None
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
try:
|
||||||
|
query_input = f"search_query: {user_query}"
|
||||||
|
query_emb = self.model.encode([query_input], convert_to_numpy=True)[0]
|
||||||
|
q_norm = np.linalg.norm(query_emb)
|
||||||
|
if q_norm > 0:
|
||||||
|
query_emb = query_emb / q_norm
|
||||||
|
|
||||||
|
best_persona = None
|
||||||
|
best_score = -1.0
|
||||||
|
|
||||||
|
for persona, proto_embs in self.prototype_embeddings.items():
|
||||||
|
scores = np.dot(proto_embs, query_emb)
|
||||||
|
max_score = np.max(scores)
|
||||||
|
if max_score > best_score:
|
||||||
|
best_score = max_score
|
||||||
|
best_persona = persona
|
||||||
|
|
||||||
|
if best_score >= threshold:
|
||||||
|
return best_persona
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
class ActiveAgentState:
|
||||||
|
def __init__(self):
|
||||||
|
self.client = httpx.AsyncClient(timeout=120.0)
|
||||||
|
self.grammar = None
|
||||||
|
self.message_history = deque()
|
||||||
|
self.mcp_server_process = None
|
||||||
|
self.jsonrpc_id = 1
|
||||||
|
self.current_persona = "orchestrator" # <-- Track the active persona
|
||||||
|
|
||||||
|
# Self-healing GBNF initialization (forced overwrite to ensure strict mode)
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(GRAMMAR_FILE_PATH) or ".", exist_ok=True)
|
||||||
|
with open(GRAMMAR_FILE_PATH, "w") as f:
|
||||||
|
f.write("""root ::= object
|
||||||
|
object ::= "{" ws "\\"thought\\":" ws string "," ws "\\"tool_call\\":" ws tool-call ws "}"
|
||||||
|
tool-call ::= "{" ws "\\"name\\":" ws string "," ws "\\"arguments\\":" ws tool-args ws "}"
|
||||||
|
tool-args ::= "{" ws ( pair ("," ws pair)* )? ws "}"
|
||||||
|
pair ::= string ws ":" ws value
|
||||||
|
value ::= string | number | "true" | "false" | "null" | tool-args | array
|
||||||
|
array ::= "[" ws ( value ("," ws value)* )? ws "]"
|
||||||
|
string ::= "\\"" ([^"\\\\\\x00-\\x1F] | "\\\\" (["\\\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]))* "\\""
|
||||||
|
number ::= "-"? ([0-9]+ | [0-9]* "." [0-9]+) ([eE] [+-]? [0-9]+)?
|
||||||
|
ws ::= [ \\t\\n\\r]*
|
||||||
|
""")
|
||||||
|
print(f"{GREEN}[Init] Dynamically recovered missing GBNF file at: {GRAMMAR_FILE_PATH}{RESET}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{RED}[Init] Failed to bootstrap GBNF file: {str(e)}{RESET}")
|
||||||
|
|
||||||
|
if os.path.exists(GRAMMAR_FILE_PATH):
|
||||||
|
with open(GRAMMAR_FILE_PATH, "r") as f:
|
||||||
|
self.grammar = f.read()
|
||||||
|
|
||||||
|
if os.path.exists(SYSTEM_PROMPT_PATH):
|
||||||
|
with open(SYSTEM_PROMPT_PATH, "r") as f:
|
||||||
|
self.system_prompt = f.read().strip()
|
||||||
|
else:
|
||||||
|
self.system_prompt = "You are a helpful assistant. Output must strictly match GBNF JSON grammar."
|
||||||
|
|
||||||
|
# Inject Master Orchestrator persona knowledge
|
||||||
|
self.system_prompt += MASTER_ORCHESTRATOR_INSTRUCTION
|
||||||
|
self.message_history.append({"role": "system", "content": self.system_prompt})
|
||||||
|
atexit.register(self.shutdown_mcp_server)
|
||||||
|
|
||||||
|
async def _drain_mcp_stderr(self, process: asyncio.subprocess.Process):
|
||||||
|
"""Asynchronously drains the stderr of the MCP subprocess to prevent buffer-blocking."""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
line = await process.stderr.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
decoded = line.decode('utf-8', errors='replace').strip()
|
||||||
|
if decoded:
|
||||||
|
print(f"{DIM}{RED}[MCP STDERR] {decoded}{RESET}")
|
||||||
|
log_telemetry("MCP_STDERR", decoded)
|
||||||
|
except Exception as e:
|
||||||
|
log_telemetry("MCP_STDERR_CRASH", str(e))
|
||||||
|
|
||||||
|
async def launch_mcp_server(self) -> bool:
|
||||||
|
"""Spawns the long-running geoscaper MCP server subprocess over persistent pipes."""
|
||||||
|
server_path = None
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
workspace_root = os.getcwd()
|
||||||
|
|
||||||
|
# Build comprehensive resolution path checklist
|
||||||
|
candidate_paths = [
|
||||||
|
os.path.join(workspace_root, "tools", "geoscaper.py"),
|
||||||
|
os.path.join(workspace_root, "agents", "tools", "geoscaper.py"),
|
||||||
|
os.path.join(workspace_root, "agents", "modules", "geoscaper", "geoscaper.py"),
|
||||||
|
os.path.join(workspace_root, "geoscaper.py"),
|
||||||
|
os.path.join(workspace_root, "core", "geoscaper.py"),
|
||||||
|
os.path.join(script_dir, "geoscaper.py"),
|
||||||
|
os.path.join(script_dir, "tools", "geoscaper.py"),
|
||||||
|
os.path.join(script_dir, "..", "tools", "geoscaper.py"),
|
||||||
|
os.path.join(script_dir, "..", "agents", "tools", "geoscaper.py"),
|
||||||
|
os.path.join(script_dir, "..", "agents", "modules", "geoscaper", "geoscaper.py"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Deduplicate paths keeping order
|
||||||
|
seen = set()
|
||||||
|
deduped_candidates = []
|
||||||
|
for p in candidate_paths:
|
||||||
|
normalized = os.path.normpath(p)
|
||||||
|
if normalized not in seen:
|
||||||
|
seen.add(normalized)
|
||||||
|
deduped_candidates.append(normalized)
|
||||||
|
|
||||||
|
for p in deduped_candidates:
|
||||||
|
if os.path.exists(p):
|
||||||
|
server_path = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if not server_path:
|
||||||
|
print(f"{RED}[ERR] MCP server binary 'geoscaper.py' not found in workspace.{RESET}")
|
||||||
|
print(f"{DIM}Searched the following locations:{RESET}")
|
||||||
|
for p in deduped_candidates:
|
||||||
|
print(f"{DIM} - {p}{RESET}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set up environment variables so subprocess can resolve local modules like lib.*
|
||||||
|
env = os.environ.copy()
|
||||||
|
additional_paths = [workspace_root, os.path.dirname(server_path)]
|
||||||
|
if "PYTHONPATH" in env:
|
||||||
|
env["PYTHONPATH"] = os.pathsep.join(additional_paths) + os.pathsep + env["PYTHONPATH"]
|
||||||
|
else:
|
||||||
|
env["PYTHONPATH"] = os.pathsep.join(additional_paths)
|
||||||
|
|
||||||
|
self.mcp_server_process = await asyncio.create_subprocess_exec(
|
||||||
|
sys.executable, server_path,
|
||||||
|
stdin=asyncio.subprocess.PIPE,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
env=env
|
||||||
|
)
|
||||||
|
# Route stderr through background task to prevent deadlock
|
||||||
|
asyncio.create_task(self._drain_mcp_stderr(self.mcp_server_process))
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{RED}[ERR] Failed to boot persistent MCP tool server: {str(e)}{RESET}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def call_mcp_tool(self, tool_name: str, arguments: Dict[str, Any]) -> str:
|
||||||
|
"""Sends a JSON-RPC 2.0 tools/call request to the persistent MCP server and receives response."""
|
||||||
|
if not self.mcp_server_process or self.mcp_server_process.returncode is not None:
|
||||||
|
await self.launch_mcp_server()
|
||||||
|
|
||||||
|
self.jsonrpc_id += 1
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": tool_name,
|
||||||
|
"arguments": arguments
|
||||||
|
},
|
||||||
|
"id": self.jsonrpc_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = json.dumps(request) + "\n"
|
||||||
|
self.mcp_server_process.stdin.write(payload.encode('utf-8'))
|
||||||
|
await self.mcp_server_process.stdin.drain()
|
||||||
|
|
||||||
|
response = None
|
||||||
|
for _ in range(50): # Bounded retry to skip arbitrary stdout debug prints
|
||||||
|
stdout_line = await self.mcp_server_process.stdout.readline()
|
||||||
|
if not stdout_line:
|
||||||
|
return json.dumps({"status": "error", "reason": "MCP tool server disconnected abruptly."})
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = json.loads(stdout_line.decode('utf-8').strip())
|
||||||
|
if "jsonrpc" in parsed and parsed.get("id") == request["id"]:
|
||||||
|
response = parsed
|
||||||
|
break
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
return json.dumps({"status": "error", "reason": "Failed to parse valid JSON-RPC response from MCP server."})
|
||||||
|
|
||||||
|
if "error" in response:
|
||||||
|
return json.dumps({"status": "error", "reason": response["error"].get("message")})
|
||||||
|
|
||||||
|
content_items = response.get("result", {}).get("content", [])
|
||||||
|
for item in content_items:
|
||||||
|
if item.get("type") == "text":
|
||||||
|
return item.get("text")
|
||||||
|
|
||||||
|
return json.dumps({"status": "error", "reason": "Invalid MCP response envelope."})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "reason": f"MCP communication error: {str(e)}"})
|
||||||
|
|
||||||
|
def shutdown_mcp_server(self):
|
||||||
|
"""Teardown hook ensuring the background MCP server process is killed."""
|
||||||
|
if self.mcp_server_process and self.mcp_server_process.returncode is None:
|
||||||
|
try:
|
||||||
|
self.mcp_server_process.terminate()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def budget(self):
|
||||||
|
while True:
|
||||||
|
total_tokens = 0
|
||||||
|
for msg in self.message_history:
|
||||||
|
if "_token_count" not in msg:
|
||||||
|
msg["_token_count"] = await count_tokens(self.client, msg["content"])
|
||||||
|
total_tokens += msg["_token_count"]
|
||||||
|
if total_tokens <= SAFETY_THRESHOLD or len(self.message_history) <= 3:
|
||||||
|
break
|
||||||
|
if len(self.message_history) >= 4:
|
||||||
|
sys_msg = self.message_history.popleft()
|
||||||
|
self.message_history.popleft()
|
||||||
|
if self.message_history and self.message_history[0]["role"] == "assistant":
|
||||||
|
self.message_history.popleft()
|
||||||
|
self.message_history.appendleft(sys_msg)
|
||||||
|
|
||||||
|
async def infer(self) -> AsyncGenerator[Dict[str, Any], None]:
|
||||||
|
clean_history = [{"role": m["role"], "content": m["content"]} for m in self.message_history]
|
||||||
|
payload = {
|
||||||
|
"messages": clean_history,
|
||||||
|
"max_tokens": MAX_TOKENS,
|
||||||
|
"stream": True,
|
||||||
|
"temperature": 0.2,
|
||||||
|
}
|
||||||
|
if self.grammar:
|
||||||
|
payload["grammar"] = self.grammar
|
||||||
|
|
||||||
|
full_buffer = ""
|
||||||
|
try:
|
||||||
|
async with self.client.stream("POST", f"{BACKEND_URL}/v1/chat/completions", json=payload) as response:
|
||||||
|
if response.status_code != 200:
|
||||||
|
await response.aread()
|
||||||
|
yield {"type": "error", "raw": f"HTTP {response.status_code}"}
|
||||||
|
return
|
||||||
|
|
||||||
|
is_streaming = "text/event-stream" in response.headers.get("content-type", "").lower()
|
||||||
|
if is_streaming:
|
||||||
|
async for line in response.aiter_lines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or not line.startswith("data:"): continue
|
||||||
|
data_str = line[5:].strip()
|
||||||
|
if data_str == "[DONE]": continue
|
||||||
|
try:
|
||||||
|
chunk = json.loads(data_str)
|
||||||
|
delta = chunk["choices"][0]["delta"].get("content") or chunk["choices"][0]["delta"].get("reasoning_content") or ""
|
||||||
|
full_buffer += delta
|
||||||
|
yield {"type": "token", "delta": delta}
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
await response.aread()
|
||||||
|
try:
|
||||||
|
result = response.json()
|
||||||
|
content = result["choices"][0]["message"].get("content") or result["choices"][0]["message"].get("reasoning_content") or ""
|
||||||
|
full_buffer = content
|
||||||
|
yield {"type": "token", "delta": content}
|
||||||
|
except Exception as e:
|
||||||
|
yield {"type": "error", "raw": str(e)}
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
yield {"type": "error", "raw": str(e)}
|
||||||
|
return
|
||||||
|
|
||||||
|
parsed_json = extract_json_from_text(full_buffer)
|
||||||
|
if parsed_json is not None:
|
||||||
|
yield {"type": "final", "data": parsed_json, "raw": full_buffer}
|
||||||
|
else:
|
||||||
|
yield {"type": "error", "raw": "JSON Parse Failure", "raw_buffer": full_buffer}
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self.client.aclose()
|
||||||
|
self.shutdown_mcp_server()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_input(prompt_string: str) -> str:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
return await loop.run_in_executor(None, input, prompt_string)
|
||||||
|
|
||||||
|
|
||||||
|
async def app_loop():
|
||||||
|
engine = ActiveAgentState()
|
||||||
|
|
||||||
|
print(f"{CYAN}[Init] Spawning persistent Model Context Protocol (MCP) server...{RESET}")
|
||||||
|
if not await engine.launch_mcp_server():
|
||||||
|
print(f"{RED}[ERR] Tool engine aborted due to MCP failure.{RESET}")
|
||||||
|
await engine.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.stdout.write(CLEAR)
|
||||||
|
print(f"{CYAN}{BOLD}=== Model Context Protocol TUI Shell ==={RESET}")
|
||||||
|
print(f"{DIM}Transport: Persistent STDIO | RPC: JSON-RPC 2.0{RESET}")
|
||||||
|
print(DIVIDER)
|
||||||
|
|
||||||
|
router = CPUIntentRouter()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
user_input = await async_input(f"{YELLOW}{BOLD}User >{RESET} ")
|
||||||
|
if user_input.lower() in ["exit", "quit", "q"]: break
|
||||||
|
if not user_input.strip(): continue
|
||||||
|
|
||||||
|
# Semantic Intent Routing Check
|
||||||
|
routed_persona = router.route_intent(user_input, threshold=0.72)
|
||||||
|
if routed_persona and engine.current_persona != routed_persona:
|
||||||
|
persona_name = routed_persona.split("/")[-1].capitalize()
|
||||||
|
print(f"\n{CYAN}{BOLD}[Router]{RESET} Input matches intent for {CYAN}{BOLD}{persona_name}{RESET} (Semantic match found).")
|
||||||
|
confirm = await async_input(f" Activate the {persona_name} persona to proceed? (Y/n): ")
|
||||||
|
if not confirm.strip() or confirm.lower().startswith('y'):
|
||||||
|
print(f"{CYAN}[Router] Dynamically switching context to {persona_name}...{RESET}")
|
||||||
|
persona_path = PERSONA_MAP.get(routed_persona)
|
||||||
|
if persona_path and os.path.exists(persona_path):
|
||||||
|
try:
|
||||||
|
with open(persona_path, "r", encoding="utf-8") as pf:
|
||||||
|
new_system = pf.read().strip()
|
||||||
|
engine.system_prompt = new_system
|
||||||
|
engine.message_history[0] = {"role": "system", "content": engine.system_prompt}
|
||||||
|
engine.current_persona = routed_persona
|
||||||
|
print(f"{GREEN}[Router] {persona_name} Activated successfully.{RESET}\n")
|
||||||
|
log_telemetry("ROUTER", f"Activated persona: {persona_name} from intent match.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{RED}[ERR] Failed to load persona: {str(e)}{RESET}\n")
|
||||||
|
log_telemetry("ROUTER_ERROR", f"Failed to load persona: {str(e)}")
|
||||||
|
else:
|
||||||
|
print(f"{RED}[ERR] Persona template not found at: {persona_path}{RESET}\n")
|
||||||
|
log_telemetry("ROUTER_ERROR", f"Persona template not found at: {persona_path}")
|
||||||
|
|
||||||
|
engine.message_history.append({"role": "user", "content": user_input})
|
||||||
|
# Autonomous agent reasoning loop
|
||||||
|
while True:
|
||||||
|
await engine.budget()
|
||||||
|
|
||||||
|
print(f"\n{GREEN}{BOLD}Assistant >{RESET} ", end="")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
final_payload = None
|
||||||
|
stream_filter = JSONStreamFilter()
|
||||||
|
|
||||||
|
async for update in engine.infer():
|
||||||
|
if update["type"] == "token":
|
||||||
|
clean_delta = stream_filter.feed(update["delta"])
|
||||||
|
if clean_delta:
|
||||||
|
sys.stdout.write(clean_delta)
|
||||||
|
sys.stdout.flush()
|
||||||
|
elif update["type"] == "final":
|
||||||
|
final_payload = update["data"]
|
||||||
|
raw_buffer = update.get("raw", "")
|
||||||
|
log_telemetry(engine.current_persona, f"RAW INFERENCE:\n{raw_buffer}\n\nPARSED JSON:\n{json.dumps(final_payload, indent=2)}")
|
||||||
|
elif update["type"] == "error":
|
||||||
|
print(f"\n{RED}[ERR] {update['raw']}{RESET}")
|
||||||
|
raw_buffer = update.get("raw_buffer", "")
|
||||||
|
log_telemetry(f"{engine.current_persona}_ERROR", f"RAW INFERENCE:\n{raw_buffer}\nHTTP ERROR: {update['raw']}")
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"\n{DIVIDER}")
|
||||||
|
if not final_payload:
|
||||||
|
break # Stop execution loop on inference error
|
||||||
|
|
||||||
|
tool_call = final_payload.get("tool_call")
|
||||||
|
|
||||||
|
# Cognitive Fail-Safe / Heuristic Backup
|
||||||
|
# If the model failed to output a structured tool_call block but explicitly stated its
|
||||||
|
# intention to activate a persona inside its "thought", we automatically heal and execute the action.
|
||||||
|
if not tool_call or tool_call == "null" or (isinstance(tool_call, dict) and not tool_call.get("name")):
|
||||||
|
thought_text = final_payload.get("thought", "").lower()
|
||||||
|
|
||||||
|
# Robust root-word check matching diverse conjugations (activating, handoff, switch, etc.)
|
||||||
|
is_switch = any(x in thought_text for x in ["activat", "switch", "hand", "persona", "run"])
|
||||||
|
|
||||||
|
if "planner" in thought_text and is_switch and engine.current_persona != "geoscaper/team/planner":
|
||||||
|
tool_call = {
|
||||||
|
"name": "activate_persona",
|
||||||
|
"arguments": {"persona": "geoscaper/team/planner"}
|
||||||
|
}
|
||||||
|
elif "builder" in thought_text and is_switch and engine.current_persona != "geoscaper/team/builder":
|
||||||
|
tool_call = {
|
||||||
|
"name": "activate_persona",
|
||||||
|
"arguments": {"persona": "geoscaper/team/builder"}
|
||||||
|
}
|
||||||
|
elif any(x in thought_text for x in ["reviewer", "audit"]) and is_switch and engine.current_persona != "geoscaper/team/reviewer":
|
||||||
|
tool_call = {
|
||||||
|
"name": "activate_persona",
|
||||||
|
"arguments": {"persona": "geoscaper/team/reviewer"}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
print(f"{YELLOW}[Fail-Safe] Agent forgot to execute a tool. Forcing retry...{RESET}")
|
||||||
|
log_telemetry(engine.current_persona, f"FAIL-SAFE TRIGGERED: Agent forgot to execute a tool. RAW INFERENCE: {json.dumps(final_payload)}")
|
||||||
|
engine.message_history.append({"role": "assistant", "content": json.dumps(final_payload)})
|
||||||
|
engine.message_history.append({"role": "user", "content": "[SYSTEM DICTATE: You failed to output a tool_call. You MUST invoke a tool on every turn. If you need to speak to the user or ask a question, use the 'ask_user' tool. Do not output tool_call: null.]"})
|
||||||
|
continue
|
||||||
|
|
||||||
|
if tool_call and isinstance(tool_call, dict) and tool_call.get("name"):
|
||||||
|
tool_name = tool_call["name"]
|
||||||
|
tool_args = tool_call.get("arguments", {})
|
||||||
|
|
||||||
|
print(f"\n{YELLOW}{BOLD}▶ Executing MCP Call:{RESET} {tool_name}")
|
||||||
|
log_telemetry(engine.current_persona, f"TOOL CALL: {tool_name}\nARGS: {json.dumps(tool_args, indent=2)}")
|
||||||
|
|
||||||
|
# Dynamic Persona Handoff (Orchestrator level virtual tools)
|
||||||
|
if tool_name == "activate_persona":
|
||||||
|
persona_key = tool_args.get("persona")
|
||||||
|
persona_path = PERSONA_MAP.get(persona_key)
|
||||||
|
if persona_path and os.path.exists(persona_path):
|
||||||
|
try:
|
||||||
|
with open(persona_path, "r", encoding="utf-8") as pf:
|
||||||
|
new_system = pf.read().strip()
|
||||||
|
engine.system_prompt = new_system
|
||||||
|
engine.message_history[0] = {"role": "system", "content": engine.system_prompt}
|
||||||
|
engine.current_persona = persona_key # <-- Track active persona
|
||||||
|
tool_result = json.dumps({"status": "success", "message": f"Activated persona: {persona_key}"})
|
||||||
|
except Exception as e:
|
||||||
|
tool_result = json.dumps({"status": "error", "reason": f"Failed to load persona file: {str(e)}"})
|
||||||
|
else:
|
||||||
|
tool_result = json.dumps({"status": "error", "reason": f"Persona '{persona_key}' not found."})
|
||||||
|
|
||||||
|
elif tool_name == "deactivate_persona":
|
||||||
|
try:
|
||||||
|
if os.path.exists(SYSTEM_PROMPT_PATH):
|
||||||
|
with open(SYSTEM_PROMPT_PATH, "r", encoding="utf-8") as pf:
|
||||||
|
new_system = pf.read().strip()
|
||||||
|
else:
|
||||||
|
new_system = "You are a helpful assistant. Output must strictly match GBNF JSON grammar."
|
||||||
|
|
||||||
|
new_system += MASTER_ORCHESTRATOR_INSTRUCTION
|
||||||
|
engine.system_prompt = new_system
|
||||||
|
engine.message_history[0] = {"role": "system", "content": engine.system_prompt}
|
||||||
|
engine.current_persona = "orchestrator" # <-- Reset to orchestrator
|
||||||
|
tool_result = json.dumps({"status": "success", "message": "Deactivated persona. Returned to Master Orchestrator."})
|
||||||
|
except Exception as e:
|
||||||
|
tool_result = json.dumps({"status": "error", "reason": f"Failed to restore master persona: {str(e)}"})
|
||||||
|
|
||||||
|
elif tool_name == "ask_user":
|
||||||
|
message = tool_args.get("message", "")
|
||||||
|
print(f"\n{CYAN}{BOLD}Agent Message:{RESET} {message}")
|
||||||
|
engine.message_history.append({"role": "assistant", "content": json.dumps(final_payload)})
|
||||||
|
break # Exit autonomous loop and wait for next user command
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Execute the tool via Model Context Protocol JSON-RPC
|
||||||
|
tool_result = await engine.call_mcp_tool(tool_name, tool_args)
|
||||||
|
|
||||||
|
print(f"{GREEN}{BOLD}◀ Output:{RESET}\n {tool_result}\n{DIVIDER}")
|
||||||
|
log_telemetry(engine.current_persona, f"TOOL RESULT: {tool_name}\nOUTPUT:\n{tool_result}")
|
||||||
|
|
||||||
|
engine.message_history.append({"role": "assistant", "content": json.dumps(final_payload)})
|
||||||
|
|
||||||
|
# Wrap the raw tool output in a SYSTEM DICTATE to force autonomous continuation
|
||||||
|
tool_content = (
|
||||||
|
f"[SYSTEM: Tool Execution Result]\n"
|
||||||
|
f"{tool_result}\n\n"
|
||||||
|
f"[SYSTEM DICTATE: Proceed to the next step of your instructions. You MUST output a valid JSON tool_call on this turn. If your instructions require an interview or question, use the 'ask_user' tool immediately. Otherwise, invoke the next autonomous tool.]"
|
||||||
|
)
|
||||||
|
engine.message_history.append({"role": "user", "content": tool_content})
|
||||||
|
|
||||||
|
# Continue loop to process the tool output autonomously
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
engine.message_history.append({"role": "assistant", "content": json.dumps(final_payload)})
|
||||||
|
break # Exit autonomous loop and wait for next user command
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\n{RED}[!] Interrupted.{RESET}")
|
||||||
|
engine.shutdown_mcp_server()
|
||||||
|
os._exit(0)
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if not os.path.exists("agents"):
|
||||||
|
print(f"{RED}[ERR] Must run from workspace root containing '/agents'.{RESET}")
|
||||||
|
sys.exit(1)
|
||||||
|
asyncio.run(app_loop())
|
||||||
150
core/tui.py
Normal file
150
core/tui.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
# --- ANSI Styling Palette ---
|
||||||
|
CYAN = "\033[96m"
|
||||||
|
GREEN = "\033[92m"
|
||||||
|
YELLOW = "\033[93m"
|
||||||
|
RED = "\033[91m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
CLEAR = "\033[2J\033[H"
|
||||||
|
|
||||||
|
# --- ANSI Layout Sequences ---
|
||||||
|
SAVE_CURSOR = "\033[s"
|
||||||
|
RESTORE_CURSOR = "\033[u"
|
||||||
|
CLEAR_LINE = "\033[K"
|
||||||
|
|
||||||
|
class TerminalUIManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.term_width, self.term_height = os.get_terminal_size()
|
||||||
|
self.current_persona = "Orchestrator"
|
||||||
|
self.active_tool = None
|
||||||
|
self.token_usage = 0
|
||||||
|
self.max_tokens = 16384
|
||||||
|
self.is_thinking = False
|
||||||
|
self.has_set_region = False
|
||||||
|
|
||||||
|
def setup_screen(self):
|
||||||
|
"""Initializes the terminal layout with a scrolling region."""
|
||||||
|
sys.stdout.write(CLEAR)
|
||||||
|
sys.stdout.flush()
|
||||||
|
# Set scrolling region from row 2 to (height - 1)
|
||||||
|
self.term_width, self.term_height = os.get_terminal_size()
|
||||||
|
sys.stdout.write(f"\033[2;{self.term_height - 1}r")
|
||||||
|
# Move cursor to row 2
|
||||||
|
sys.stdout.write("\033[2;1H")
|
||||||
|
sys.stdout.flush()
|
||||||
|
self.has_set_region = True
|
||||||
|
self.redraw_header()
|
||||||
|
self.redraw_footer()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Restores the terminal scrolling region."""
|
||||||
|
if self.has_set_region:
|
||||||
|
sys.stdout.write("\033[r") # Reset scrolling region
|
||||||
|
sys.stdout.write(f"\033[{self.term_height};1H")
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def update_state(self, persona=None, active_tool=None, token_usage=None):
|
||||||
|
"""Update internal state and redraw header/footer if necessary."""
|
||||||
|
changed_header = False
|
||||||
|
changed_footer = False
|
||||||
|
|
||||||
|
if persona is not None and persona != self.current_persona:
|
||||||
|
self.current_persona = persona
|
||||||
|
changed_header = True
|
||||||
|
|
||||||
|
if active_tool is not None and active_tool != self.active_tool:
|
||||||
|
self.active_tool = active_tool
|
||||||
|
changed_footer = True
|
||||||
|
|
||||||
|
if token_usage is not None and token_usage != self.token_usage:
|
||||||
|
self.token_usage = token_usage
|
||||||
|
changed_footer = True
|
||||||
|
|
||||||
|
if changed_header:
|
||||||
|
self.redraw_header()
|
||||||
|
if changed_footer:
|
||||||
|
self.redraw_footer()
|
||||||
|
|
||||||
|
def redraw_header(self):
|
||||||
|
"""Draws the fixed top header."""
|
||||||
|
sys.stdout.write(SAVE_CURSOR)
|
||||||
|
sys.stdout.write("\033[1;1H") # Move to top left
|
||||||
|
|
||||||
|
persona_name = self.current_persona.split('/')[-1].capitalize()
|
||||||
|
header_text = f"⚙ SmarterAgents │ Persona: {persona_name}"
|
||||||
|
padding = max(0, self.term_width - len(header_text) - 1)
|
||||||
|
|
||||||
|
sys.stdout.write(f"{CYAN}{BOLD}{header_text} {'─' * padding}{RESET}{CLEAR_LINE}")
|
||||||
|
sys.stdout.write(RESTORE_CURSOR)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def redraw_footer(self):
|
||||||
|
"""Draws the fixed bottom footer."""
|
||||||
|
sys.stdout.write(SAVE_CURSOR)
|
||||||
|
sys.stdout.write(f"\033[{self.term_height};1H") # Move to bottom left
|
||||||
|
|
||||||
|
# Calculate budget bar
|
||||||
|
pct = min(1.0, self.token_usage / self.max_tokens) if self.max_tokens > 0 else 0
|
||||||
|
filled = int(pct * 10)
|
||||||
|
empty = 10 - filled
|
||||||
|
bar = "█" * filled + "░" * empty
|
||||||
|
pct_str = f"{int(pct * 100)}%"
|
||||||
|
|
||||||
|
status_text = "Idle"
|
||||||
|
if self.active_tool:
|
||||||
|
status_text = f"⚡ Running Tool: {self.active_tool}"
|
||||||
|
|
||||||
|
footer_text = f" Token Budget: [{bar}] {pct_str} │ Status: {status_text} "
|
||||||
|
padding = max(0, self.term_width - len(footer_text))
|
||||||
|
|
||||||
|
sys.stdout.write(f"{DIM}{footer_text}{' ' * padding}{RESET}{CLEAR_LINE}")
|
||||||
|
sys.stdout.write(RESTORE_CURSOR)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def start_thought(self):
|
||||||
|
"""Starts rendering a thought block."""
|
||||||
|
if not self.is_thinking:
|
||||||
|
self.is_thinking = True
|
||||||
|
sys.stdout.write(f"\n{CYAN}{DIM}[💭 Agent Thinking...]\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def end_thought(self):
|
||||||
|
"""Ends rendering a thought block."""
|
||||||
|
if self.is_thinking:
|
||||||
|
self.is_thinking = False
|
||||||
|
sys.stdout.write(f"{RESET}\n{CYAN}{DIM}[✓ Thought Complete]{RESET}\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def stream_thought_chunk(self, chunk):
|
||||||
|
"""Streams raw thought characters."""
|
||||||
|
sys.stdout.write(chunk)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def print_message(self, message, role="Assistant"):
|
||||||
|
"""Prints a standard message."""
|
||||||
|
color = GREEN if role == "Assistant" else YELLOW
|
||||||
|
sys.stdout.write(f"\n{color}{BOLD}{role} >{RESET} {message}\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def log_tool_call(self, tool_name, args):
|
||||||
|
"""Prints a nicely formatted tool invocation box."""
|
||||||
|
sys.stdout.write(f"\n{YELLOW}{BOLD}╭─[🔧 Tool Execution: {tool_name}]" + "─" * max(0, self.term_width - 25 - len(tool_name)) + f"{RESET}\n")
|
||||||
|
try:
|
||||||
|
formatted_args = json.dumps(args, indent=2)
|
||||||
|
for line in formatted_args.splitlines():
|
||||||
|
sys.stdout.write(f"{YELLOW}│{RESET} {line}\n")
|
||||||
|
except:
|
||||||
|
sys.stdout.write(f"{YELLOW}│{RESET} {args}\n")
|
||||||
|
sys.stdout.write(f"{YELLOW}╰" + "─" * (self.term_width - 1) + f"{RESET}\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def print_system_info(self, message, is_error=False):
|
||||||
|
color = RED if is_error else CYAN
|
||||||
|
sys.stdout.write(f"\n{color}{BOLD}[System]{RESET} {message}\n")
|
||||||
|
sys.stdout.flush()
|
||||||
131
start.sh
Executable file
131
start.sh
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
# --- Color Definitions ---
|
||||||
|
CYAN='\033[96m'
|
||||||
|
GREEN='\033[92m'
|
||||||
|
YELLOW='\033[93m'
|
||||||
|
RED='\033[91m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
|
||||||
|
# --- Paths & Dependencies ---
|
||||||
|
MODEL_DIR="./models"
|
||||||
|
AGENT_DIR="./agents"
|
||||||
|
LLAMA_SERVER="./core/llama.cpp/build/bin/llama-server"
|
||||||
|
PYTHON_SCRIPT="./core/smarterframework.py"
|
||||||
|
|
||||||
|
echo -e "${CYAN}${BOLD}=== SmarterAgents Core Launch System ===${RESET}"
|
||||||
|
|
||||||
|
# Ensure local dev and log folders exist in workspace
|
||||||
|
mkdir -p dev
|
||||||
|
mkdir -p core/logs
|
||||||
|
|
||||||
|
# Verify core compilation target exists
|
||||||
|
if [ ! -f "$LLAMA_SERVER" ]; then
|
||||||
|
echo -e "${RED}[ERR] compiled 'llama-server' binary missing at '$LLAMA_SERVER'.${RESET}"
|
||||||
|
echo -e "Please run cmake build inside core/llama.cpp as detailed in the README."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Model Selection
|
||||||
|
mapfile -t MODELS < <(find "$MODEL_DIR" -name "*.gguf" 2>/dev/null)
|
||||||
|
if [ ${#MODELS[@]} -eq 0 ]; then
|
||||||
|
echo -e "${RED}[ERR] No GGUF models discovered in '$MODEL_DIR'. Please copy a model there first.${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}Discovered Models:${RESET}"
|
||||||
|
for i in "${!MODELS[@]}"; do
|
||||||
|
echo -e " [$i] $(basename "${MODELS[$i]}")"
|
||||||
|
done
|
||||||
|
read -rp "Select Model Index [0-$((${#MODELS[@]} - 1))]: " MODEL_IDX
|
||||||
|
SELECTED_MODEL="${MODELS[$MODEL_IDX]}"
|
||||||
|
|
||||||
|
# 2. Agent Selection
|
||||||
|
mapfile -t AGENTS < <(find "$AGENT_DIR" -name "*.md" 2>/dev/null)
|
||||||
|
if [ ${#AGENTS[@]} -eq 0 ]; then
|
||||||
|
echo -e "${RED}[ERR] No Markdown agents discovered in '$AGENT_DIR'.${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}Discovered Agent Personas:${RESET}"
|
||||||
|
for i in "${!AGENTS[@]}"; do
|
||||||
|
echo -e " [$i] $(basename "${AGENTS[$i]}")"
|
||||||
|
done
|
||||||
|
read -rp "Select Agent Index [0-$((${#AGENTS[@]} - 1))]: " AGENT_IDX
|
||||||
|
SELECTED_AGENT="${AGENTS[$AGENT_IDX]}"
|
||||||
|
|
||||||
|
# 3. Environment Variable Export for Python Orchestrator
|
||||||
|
GRAMMAR_FILE=""
|
||||||
|
for path in "tools/tool_rules.gbnf" "tools/tool_rules.gnbf" "agents/tools/tool_rules.gbnf" "agents/tools/tool_rules.gnbf"; do
|
||||||
|
if [ -f "$path" ]; then
|
||||||
|
GRAMMAR_FILE="$path"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$GRAMMAR_FILE" ]; then
|
||||||
|
export AMDA_GRAMMAR_FILE="$GRAMMAR_FILE"
|
||||||
|
else
|
||||||
|
export AMDA_GRAMMAR_FILE="tools/tool_rules.gbnf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export AMDA_BACKEND_URL="http://127.0.0.1:8080"
|
||||||
|
export AMDA_SYSTEM_PROMPT="$SELECTED_AGENT"
|
||||||
|
|
||||||
|
# 4. Vulkan Background Server Initialization
|
||||||
|
PHYS_CORES=8
|
||||||
|
|
||||||
|
echo -e "\n${CYAN}[Init] Booting Vulkan llama-server daemon...${RESET}"
|
||||||
|
echo -e "${DIM}Assigning 100% compute threads to $PHYS_CORES physical cores.${RESET}"
|
||||||
|
|
||||||
|
# Start llama-server and capture process PID
|
||||||
|
"$LLAMA_SERVER" \
|
||||||
|
--model "$SELECTED_MODEL" \
|
||||||
|
--host "127.0.0.1" \
|
||||||
|
--port "8080" \
|
||||||
|
--ctx-size 16384 \
|
||||||
|
--parallel 1 \
|
||||||
|
--threads "$PHYS_CORES" \
|
||||||
|
--batch-size 128 \
|
||||||
|
--ubatch-size 64 \
|
||||||
|
--no-mmap \
|
||||||
|
-ngl 999 \
|
||||||
|
-fa on \
|
||||||
|
-ctk q8_0 \
|
||||||
|
-ctv q8_0 \
|
||||||
|
--jinja > core/logs/llama-server.log 2>&1 & # <-- Added --jinja here
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Safe Target-Specific cleanup (Only kill the llama-server child process)
|
||||||
|
cleanup() {
|
||||||
|
echo -e "\n${YELLOW}[Exit] Shutting down llama-server daemon... (PID: $SERVER_PID)${RESET}"
|
||||||
|
kill -9 "$SERVER_PID" 2>/dev/null || true
|
||||||
|
wait "$SERVER_PID" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM HUP
|
||||||
|
|
||||||
|
# 5. Health Check Diagnostics
|
||||||
|
echo -e "${CYAN}[Health] Polling background loop for local endpoint availability...${RESET}"
|
||||||
|
RETRIES=15
|
||||||
|
CONNECTED=false
|
||||||
|
for ((i=1; i<=RETRIES; i++)); do
|
||||||
|
if curl -s -o /dev/null -w "%{http_code}" "$AMDA_BACKEND_URL/health" | grep -q "200"; then
|
||||||
|
echo -e "${GREEN}[Ready] Local Vulkan compute engine activated successfully.${RESET}"
|
||||||
|
CONNECTED=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$CONNECTED" = false ]; then
|
||||||
|
echo -e "${RED}[ERR] llama-server failed to activate within timeout period.${RESET}"
|
||||||
|
echo -e "Diagnostics: Last 15 lines of core/logs/llama-server.log:"
|
||||||
|
tail -n 15 core/logs/llama-server.log
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Transfer Execution to Python TUI Engine
|
||||||
|
echo -e "${CYAN}[Exec] Handoff control to asynchronous user interface...${RESET}\n"
|
||||||
|
python3 "$PYTHON_SCRIPT"
|
||||||
10
tools/tool_rules.gbnf
Normal file
10
tools/tool_rules.gbnf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
root ::= object
|
||||||
|
object ::= "{" ws "\"thought\":" ws string "," ws "\"tool_call\":" ws tool-call ws "}"
|
||||||
|
tool-call ::= "{" ws "\"name\":" ws string "," ws "\"arguments\":" ws tool-args ws "}"
|
||||||
|
tool-args ::= "{" ws ( pair ("," ws pair)* )? ws "}"
|
||||||
|
pair ::= string ws ":" ws value
|
||||||
|
value ::= string | number | "true" | "false" | "null" | tool-args | array
|
||||||
|
array ::= "[" ws ( value ("," ws value)* )? ws "]"
|
||||||
|
string ::= "\"" ([^"\\\x00-\x1F] | "\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]))* "\""
|
||||||
|
number ::= "-"? ([0-9]+ | [0-9]* "." [0-9]+) ([eE] [+-]? [0-9]+)?
|
||||||
|
ws ::= [ \t\n\r]*
|
||||||
Reference in New Issue
Block a user