Intelligent Agent Router: LLM-powered natural language routing with suggestion UI

This commit is contained in:
2026-04-14 14:41:44 +00:00
parent f39bd13fc6
commit f01553c511
4 changed files with 413 additions and 1 deletions
+166 -1
View File
@@ -14,7 +14,7 @@ from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from database import get_db, init_db
from models import User, AgentCatalog, AgentInstance, Run, LLMProvider, Bridge
from models import User, AgentCatalog, AgentInstance, Run, LLMProvider, Bridge, RouteLog
app = FastAPI(title="Agent Command Center", version="2026.04.12.01")
@@ -724,6 +724,171 @@ def admin_delete_catalog(catalog_id: str, admin: dict = Depends(require_admin),
return {"status": "deleted"}
# --- Agent Router ---
class RouterRequest(BaseModel):
request: str
@app.get("/api/catalog/all")
def list_catalog_all(db: Session = Depends(get_db)):
"""Internal: full catalog for the router (no auth)."""
entries = db.query(AgentCatalog).all()
return [{
"id": e.id, "name": e.name, "description": e.description,
"category": e.category, "supports_schedule": e.supports_schedule,
"is_sub_agent": e.is_sub_agent, "requires_llm": e.requires_llm,
} for e in entries]
@app.post("/api/router")
def ask_router(data: RouterRequest, user: dict = Depends(require_auth), db: Session = Depends(get_db)):
"""Route a natural language request to the best agent."""
import subprocess
import sys
# Call the router agent
agent_dir = "/app/agents"
config_path = f"/tmp/router_{user['user_id']}_{secrets.token_hex(4)}.json"
with open(config_path, "w") as f:
json.dump({"user_id": user["user_id"], "request": data.request}, f)
try:
result = subprocess.run(
["python3", "-c",
f"import sys, json; sys.path.insert(0, '{agent_dir}'); "
f"d = json.load(open('{config_path}')); "
f"from agent_router import route; "
f"r = route(d['user_id'], d['request']); "
f"print(json.dumps(r))"],
capture_output=True, text=True, timeout=120, cwd=agent_dir,
env={**dict(os.environ), "PYTHONPATH": agent_dir},
)
os.remove(config_path)
if result.returncode != 0:
raise RuntimeError(result.stderr[:500])
recommendation = json.loads(result.stdout.strip())
except Exception as e:
return {"error": str(e), "action": "not_possible", "reasoning": f"Router error: {e}"}
# Log the route
log = RouteLog(
user_id=user["user_id"],
request_text=data.request,
recommended_agent=recommendation.get("agent_name", ""),
action=recommendation.get("action", ""),
reasoning=recommendation.get("reasoning", ""),
outcome="pending",
metadata_={
"instance_id": recommendation.get("instance_id"),
"catalog_id": recommendation.get("catalog_id"),
"config": recommendation.get("config"),
"model": recommendation.get("model", ""),
"tokens_in": recommendation.get("tokens_in", 0),
"tokens_out": recommendation.get("tokens_out", 0),
},
)
db.add(log)
db.commit()
recommendation["route_id"] = log.id
return recommendation
@app.post("/api/router/{route_id}/accept")
def accept_route(route_id: int, user: dict = Depends(require_auth), db: Session = Depends(get_db)):
"""Accept a router suggestion and execute it."""
log = db.query(RouteLog).filter(RouteLog.id == route_id, RouteLog.user_id == user["user_id"]).first()
if not log:
raise HTTPException(status_code=404)
log.outcome = "accepted"
meta = log.metadata_ or {}
action = log.action
instance_id = meta.get("instance_id")
# Execute the recommended action
if action == "run_existing" and instance_id:
# Trigger the instance
import subprocess
inst = db.query(AgentInstance).filter(AgentInstance.id == instance_id).first()
if inst:
agent_dir = "/app/agents"
catalog_id = inst.catalog_id
u = db.query(User).filter(User.id == user["user_id"]).first()
env = {**dict(os.environ), "PYTHONPATH": agent_dir}
if catalog_id == "daily-briefing":
script_map = {"eric": "eric_briefing.py", "angela": "angela_briefing.py"}
script = script_map.get(u.username)
if script:
env_key = f"{u.username.upper().replace('.', '_')}_INSTANCE_ID"
env[env_key] = str(instance_id)
subprocess.Popen(["python3", f"{agent_dir}/{script}"], env=env, cwd=agent_dir)
elif catalog_id == "project-monitor":
config_path = f"/tmp/pm_config_{instance_id}.json"
with open(config_path, "w") as f:
json.dump({"config": inst.config or {}, "user_id": user["user_id"], "instance_id": instance_id}, f)
subprocess.Popen(
["python3", "-c",
f"import sys, json; sys.path.insert(0, '{agent_dir}'); "
f"d = json.load(open('{config_path}')); "
f"from project_monitor import run; "
f"run(d['config'], user_id=d['user_id'], instance_id=d['instance_id'])"],
env=env, cwd=agent_dir,
)
db.commit()
return {"status": "executing", "message": f"Running {inst.name}"}
elif action == "configure" and instance_id:
inst = db.query(AgentInstance).filter(AgentInstance.id == instance_id).first()
if inst and meta.get("config"):
new_config = {**(inst.config or {}), **meta["config"]}
inst.config = new_config
from sqlalchemy.orm.attributes import flag_modified
flag_modified(inst, "config")
if new_config.get("project_name") and inst.catalog_id == "project-monitor":
inst.name = f"Project Monitor - {new_config['project_name']}"
db.commit()
return {"status": "configured", "message": f"Updated {inst.name}"}
elif action == "create_and_run":
catalog_id = meta.get("catalog_id")
catalog = db.query(AgentCatalog).filter(AgentCatalog.id == catalog_id).first()
if catalog:
config = {**(catalog.default_config or {}), **(meta.get("config") or {})}
inst = AgentInstance(
user_id=user["user_id"],
catalog_id=catalog_id,
name=meta.get("instance_name") or catalog.name,
config=config,
schedule="manual",
)
db.add(inst)
db.commit()
return {"status": "created", "message": f"Created and ready: {inst.name}", "instance_id": inst.id}
elif action == "info":
db.commit()
return {"status": "info", "message": log.reasoning}
db.commit()
return {"status": "accepted"}
@app.post("/api/router/{route_id}/reject")
def reject_route(route_id: int, user: dict = Depends(require_auth), db: Session = Depends(get_db)):
"""Reject a router suggestion."""
log = db.query(RouteLog).filter(RouteLog.id == route_id, RouteLog.user_id == user["user_id"]).first()
if not log:
raise HTTPException(status_code=404)
log.outcome = "rejected"
db.commit()
return {"status": "rejected"}
# --- Bridge Management ---
class BridgeRegister(BaseModel):
+14
View File
@@ -85,6 +85,20 @@ class Bridge(Base):
user = relationship("User")
class RouteLog(Base):
__tablename__ = "route_log"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
request_text = Column(Text, nullable=False)
recommended_agent = Column(String, default="")
action = Column(String, default="")
reasoning = Column(Text, default="")
outcome = Column(String, default="pending") # pending, accepted, rejected, success, failed
metadata_ = Column("metadata", JSON, default=dict)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
class LLMProvider(Base):
__tablename__ = "llm_providers"
+91
View File
@@ -95,6 +95,20 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
.bridge-bar .bridge-dot.offline{background:var(--red)}
.bridge-bar .bridge-actions{display:flex;gap:.5rem}
/* Router */
.router-bar{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:1rem 1.25rem;margin-bottom:1.5rem;display:flex;gap:.75rem;align-items:center}
.router-bar input{flex:1;padding:.6rem .85rem;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.9rem;outline:none}
.router-bar input:focus{border-color:var(--accent)}
.router-bar input::placeholder{color:var(--text-dim)}
.router-bar button{padding:.6rem 1.25rem;background:var(--accent);color:#fff;border:none;border-radius:6px;font-size:.9rem;cursor:pointer;white-space:nowrap}
.router-bar button:hover{background:var(--accent-hover)}
.router-bar button:disabled{opacity:.5;cursor:not-allowed}
.router-result{background:var(--surface);border:1px solid var(--accent);border-radius:10px;padding:1.25rem;margin-bottom:1.5rem;display:none}
.router-result .rr-action{font-size:.75rem;text-transform:uppercase;color:var(--accent);font-weight:600;letter-spacing:.04em;margin-bottom:.4rem}
.router-result .rr-agent{font-size:1.05rem;font-weight:600;margin-bottom:.5rem}
.router-result .rr-reasoning{color:var(--text-dim);font-size:.85rem;margin-bottom:1rem;line-height:1.5}
.router-result .rr-actions{display:flex;gap:.5rem}
.empty-state{text-align:center;padding:3rem;color:var(--text-dim)}
.empty-state h3{margin-bottom:.5rem;color:var(--text)}
.time-ago{color:var(--text-dim)}
@@ -114,6 +128,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
</div>
<div class="container">
<div class="router-bar">
<input type="text" id="router-input" placeholder="What do you need? Ask the router..." onkeydown="if(event.key==='Enter')askRouter()">
<button id="router-btn" onclick="askRouter()">Ask</button>
</div>
<div class="router-result" id="router-result"></div>
<div id="bridge-bar"></div>
<div class="section-header">
<h2>My Agents</h2>
@@ -361,6 +380,78 @@ async function enableAgent(catalogId,name){
else{const err=await res.json();alert(err.detail||'Failed to enable agent')}
}
// --- Router ---
async function askRouter(){
const input=document.getElementById('router-input');
const btn=document.getElementById('router-btn');
const resultDiv=document.getElementById('router-result');
const text=input.value.trim();
if(!text)return;
btn.disabled=true;btn.textContent='Thinking...';
resultDiv.style.display='none';
try{
const res=await fetch(API+'/api/router',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({request:text})});
if(res.status===401){location.href='/login';return}
const r=await res.json();
if(r.error){
resultDiv.innerHTML=`<div class="rr-action">Error</div><div class="rr-reasoning">${r.reasoning||r.error}</div>`;
resultDiv.style.display='block';
btn.disabled=false;btn.textContent='Ask';
return;
}
const action=r.action||'info';
const actionLabels={run_existing:'Run Agent',create_and_run:'Create & Run',configure:'Update Config',info:'Info',not_possible:'Not Available'};
let actionsHtml='';
if(action==='run_existing'||action==='create_and_run'||action==='configure'){
actionsHtml=`<div class="rr-actions">
<button class="btn-save" onclick="acceptRoute(${r.route_id})">Run It</button>
<button class="btn-secondary" onclick="rejectRoute(${r.route_id})">Dismiss</button>
</div>`;
} else if(action==='info'){
actionsHtml=`<div class="rr-actions"><button class="btn-secondary" onclick="dismissRouter()">OK</button></div>`;
} else {
actionsHtml=`<div class="rr-actions"><button class="btn-secondary" onclick="dismissRouter()">OK</button></div>`;
}
resultDiv.innerHTML=`
<div class="rr-action">${actionLabels[action]||action}</div>
<div class="rr-agent">${r.agent_name||''}</div>
<div class="rr-reasoning">${r.reasoning||''}</div>
${actionsHtml}`;
resultDiv.style.display='block';
}catch(e){
resultDiv.innerHTML=`<div class="rr-action">Error</div><div class="rr-reasoning">Connection error: ${e}</div>`;
resultDiv.style.display='block';
}
btn.disabled=false;btn.textContent='Ask';
}
async function acceptRoute(routeId){
const resultDiv=document.getElementById('router-result');
resultDiv.querySelector('.rr-actions').innerHTML='<span style="color:var(--green)">Running...</span>';
const res=await fetch(API+'/api/router/'+routeId+'/accept',{method:'POST'});
if(res.ok){
const data=await res.json();
resultDiv.querySelector('.rr-actions').innerHTML=`<span style="color:var(--green)">${data.message||'Done'}</span>`;
setTimeout(()=>{dismissRouter();refresh()},3000);
}
}
async function rejectRoute(routeId){
await fetch(API+'/api/router/'+routeId+'/reject',{method:'POST'});
dismissRouter();
}
function dismissRouter(){
document.getElementById('router-result').style.display='none';
document.getElementById('router-input').value='';
}
// --- LLM Settings ---
async function showLLMSettings(){
const res=await fetch(API+'/api/me/llm');