steer-sdk 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. steer/__init__.py +13 -0
  2. steer/cli.py +149 -0
  3. steer/config.py +23 -0
  4. steer/core.py +125 -0
  5. steer/llm.py +55 -0
  6. steer/mock.py +65 -0
  7. steer/schemas.py +75 -0
  8. steer/server.py +122 -0
  9. steer/storage.py +76 -0
  10. steer/ui/404.html +1 -0
  11. steer/ui/__next.__PAGE__.txt +9 -0
  12. steer/ui/__next._full.txt +20 -0
  13. steer/ui/__next._head.txt +8 -0
  14. steer/ui/__next._index.txt +6 -0
  15. steer/ui/__next._tree.txt +3 -0
  16. steer/ui/_next/static/chunks/0d03996d4dc2f4a9.js +1 -0
  17. steer/ui/_next/static/chunks/42879de7b8087bc9.js +1 -0
  18. steer/ui/_next/static/chunks/6d94cd2de0f5bc76.js +1 -0
  19. steer/ui/_next/static/chunks/752bea8c8f15cedd.js +2 -0
  20. steer/ui/_next/static/chunks/85e2cd8235bb75d4.css +2 -0
  21. steer/ui/_next/static/chunks/940e70d544422cd1.js +1 -0
  22. steer/ui/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  23. steer/ui/_next/static/chunks/d217698e32abd0dc.js +4 -0
  24. steer/ui/_next/static/chunks/turbopack-0d14a708cd8eabb2.js +3 -0
  25. steer/ui/_next/static/cxvsdwfl0GLod-qFpT8EQ/_buildManifest.js +11 -0
  26. steer/ui/_next/static/cxvsdwfl0GLod-qFpT8EQ/_clientMiddlewareManifest.json +1 -0
  27. steer/ui/_next/static/cxvsdwfl0GLod-qFpT8EQ/_ssgManifest.js +1 -0
  28. steer/ui/_next/static/media/1bffadaabf893a1e-s.7cd81963.woff2 +0 -0
  29. steer/ui/_next/static/media/2bbe8d2671613f1f-s.76dcb0b2.woff2 +0 -0
  30. steer/ui/_next/static/media/2c55a0e60120577a-s.2a48534a.woff2 +0 -0
  31. steer/ui/_next/static/media/5476f68d60460930-s.c995e352.woff2 +0 -0
  32. steer/ui/_next/static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2 +0 -0
  33. steer/ui/_next/static/media/9c72aa0f40e4eef8-s.18a48cbc.woff2 +0 -0
  34. steer/ui/_next/static/media/ad66f9afd8947f86-s.7a40eb73.woff2 +0 -0
  35. steer/ui/_next/static/media/favicon.0b3bf435.ico +0 -0
  36. steer/ui/_not-found/__next._full.txt +15 -0
  37. steer/ui/_not-found/__next._head.txt +8 -0
  38. steer/ui/_not-found/__next._index.txt +6 -0
  39. steer/ui/_not-found/__next._not-found.__PAGE__.txt +5 -0
  40. steer/ui/_not-found/__next._not-found.txt +4 -0
  41. steer/ui/_not-found/__next._tree.txt +2 -0
  42. steer/ui/_not-found.html +1 -0
  43. steer/ui/_not-found.txt +15 -0
  44. steer/ui/favicon.ico +0 -0
  45. steer/ui/file.svg +1 -0
  46. steer/ui/globe.svg +1 -0
  47. steer/ui/index.html +1 -0
  48. steer/ui/index.txt +20 -0
  49. steer/ui/next.svg +1 -0
  50. steer/ui/vercel.svg +1 -0
  51. steer/ui/window.svg +1 -0
  52. steer/utils.py +28 -0
  53. steer/verifiers.py +159 -0
  54. steer/worker.py +71 -0
  55. steer_sdk-0.1.8.dist-info/METADATA +209 -0
  56. steer_sdk-0.1.8.dist-info/RECORD +58 -0
  57. steer_sdk-0.1.8.dist-info/WHEEL +4 -0
  58. steer_sdk-0.1.8.dist-info/entry_points.txt +3 -0
steer/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from .core import capture
2
+ from .storage import rulebook
3
+ from .utils import wait_for_rules
4
+ from .mock import MockLLM # <--- NEW
5
+
6
+ __version__ = "0.1.3"
7
+
8
+ def get_context(agent_name: str) -> str:
9
+ rules = rulebook.get_rules_text(agent_name)
10
+ if not rules: return ""
11
+ return f"\n\n### STEER RELIABILITY RULES:\n{rules}\n"
12
+
13
+ __all__ = ["capture", "get_context", "wait_for_rules", "MockLLM"]
steer/cli.py ADDED
@@ -0,0 +1,149 @@
1
+ import sys
2
+ import uvicorn
3
+ import webbrowser
4
+ import argparse
5
+ import os
6
+ from rich.console import Console
7
+ from steer.server import app
8
+
9
+ console = Console()
10
+
11
+ # --- DEMO 1: USER PROFILE AGENT ---
12
+ DEMO_1_CONTENT = """import json
13
+ from steer import capture, MockLLM
14
+ from steer.verifiers import JsonVerifier
15
+
16
+ # Scenario: An agent generating data for a frontend.
17
+ json_guard = JsonVerifier(name="Strict JSON")
18
+
19
+ @capture(tags=["profile_generator"], verifiers=[json_guard])
20
+ def generate_profile(request: str, steer_rules: str = ""):
21
+ print(f"🤖 Processing request: '{request}'...")
22
+
23
+ # 1. Steer automatically injects rules into 'steer_rules'
24
+ # 2. We inject them into the System Prompt (Standard RAG/Agent pattern)
25
+ system_prompt = f"You are a backend API. Output data based on the request.\\nReliability Rules: {steer_rules}"
26
+
27
+ print(f" 🧠 System Prompt: {system_prompt.strip()}")
28
+
29
+ # 3. Call Model (Mocked for demo, replace with OpenAI in prod)
30
+ return MockLLM.call(system_prompt, request)
31
+
32
+ if __name__ == "__main__":
33
+ print("--- ⚡ Steer Demo: Profile Generator ---")
34
+ try:
35
+ generate_profile("Create active admin profile for Alice")
36
+ print("\\n✅ SUCCESS: Valid JSON returned.")
37
+ except Exception as e:
38
+ print(f"\\n🚨 BLOCKED BY STEER: {e}")
39
+ print("👉 Run 'steer ui' to fix the 'profile_generator'.")
40
+ """
41
+
42
+ # --- DEMO 2: SUPPORT BOT ---
43
+ DEMO_2_CONTENT = """from steer import capture, MockLLM
44
+ from steer.verifiers import RegexVerifier
45
+
46
+ # Scenario: A support bot summarizing tickets.
47
+ email_guard = RegexVerifier(
48
+ name="PII Shield",
49
+ pattern=r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
50
+ fail_message="Output contains visible email address."
51
+ )
52
+
53
+ @capture(tags=["support_bot"], verifiers=[email_guard])
54
+ def analyze_ticket(ticket_content: str, steer_rules: str = ""):
55
+ print(f"🤖 Analyzing: '{ticket_content}'...")
56
+
57
+ # Inject rules into context
58
+ system_prompt = f"You are a helpful support agent.\\nSecurity Protocols: {steer_rules}"
59
+ print(f" 🧠 System Prompt: {system_prompt.strip()}")
60
+
61
+ return MockLLM.call(system_prompt, ticket_content)
62
+
63
+ if __name__ == "__main__":
64
+ print("--- ⚡ Steer Demo: Support Bot ---")
65
+ try:
66
+ analyze_ticket("Ticket #994: Refund request from Alice")
67
+ print("\\n✅ SUCCESS: PII was redacted.")
68
+ except Exception as e:
69
+ print(f"\\n🚨 BLOCKED BY STEER: {e}")
70
+ print("👉 Run 'steer ui' to fix the 'support_bot'.")
71
+ """
72
+
73
+ # --- DEMO 3: WEATHER BOT ---
74
+ DEMO_3_CONTENT = """from steer import capture, MockLLM
75
+ from steer.verifiers import AmbiguityVerifier
76
+
77
+ # Scenario: A weather bot checking forecasts.
78
+ logic_guard = AmbiguityVerifier(
79
+ name="Ambiguity Check",
80
+ tool_result_key="results",
81
+ answer_key="message",
82
+ threshold=3,
83
+ required_phrase="which state"
84
+ )
85
+
86
+ @capture(tags=["weather_bot"], verifiers=[logic_guard])
87
+ def check_forecast(location: str, steer_rules: str = ""):
88
+ print(f"🤖 Checking: '{location}'...")
89
+
90
+ system_prompt = f"You are a weather bot.\\nPolicy: {steer_rules}"
91
+ print(f" 🧠 System Prompt: {system_prompt.strip()}")
92
+
93
+ return MockLLM.call(system_prompt, location)
94
+
95
+ if __name__ == "__main__":
96
+ print("--- ⚡ Steer Demo: Weather Bot ---")
97
+ try:
98
+ check_forecast("What is the weather in Springfield?")
99
+ print("\\n✅ SUCCESS: Bot asked for clarification.")
100
+ except Exception as e:
101
+ print(f"\\n🚨 BLOCKED BY STEER: {e}")
102
+ print("👉 Run 'steer ui' to fix the 'weather_bot'.")
103
+ """
104
+
105
+ def generate_demos():
106
+ files = {
107
+ "01_structure_guard.py": DEMO_1_CONTENT,
108
+ "02_safety_guard.py": DEMO_2_CONTENT,
109
+ "03_logic_guard.py": DEMO_3_CONTENT
110
+ }
111
+
112
+ console.print("\n[bold green]📦 Generating Steer Demos...[/bold green]")
113
+ for filename, content in files.items():
114
+ if not os.path.exists(filename):
115
+ with open(filename, "w") as f:
116
+ f.write(content)
117
+ console.print(f" Created [bold]{filename}[/bold]")
118
+ else:
119
+ console.print(f" [dim]Skipped {filename} (exists)[/dim]")
120
+
121
+ console.print("\n[bold]🚀 Ready![/bold] Run [green]python 01_structure_guard.py[/green] to start.")
122
+
123
+ def start_server(port=8000):
124
+ url = f"http://localhost:{port}"
125
+ console.print(f"\n[bold green]🚀 Steer Mission Control active at {url}[/bold green]")
126
+ console.print("[dim]Press Ctrl+C to stop[/dim]\n")
127
+ try:
128
+ webbrowser.open(url)
129
+ except:
130
+ pass
131
+ uvicorn.run(app, host="0.0.0.0", port=port, log_level="error")
132
+
133
+ def main():
134
+ parser = argparse.ArgumentParser(description="Steer AI - Active Reliability")
135
+ parser.add_argument("command", nargs="?", help="Command to run ('ui' or 'init')")
136
+
137
+ args = parser.parse_args()
138
+
139
+ if args.command == "ui":
140
+ start_server()
141
+ elif args.command == "init":
142
+ generate_demos()
143
+ else:
144
+ console.print("[bold]Steer AI[/bold] - The Active Reliability Layer")
145
+ console.print("Run [green]steer init[/green] to generate examples.")
146
+ console.print("Run [green]steer ui[/green] to start the dashboard.")
147
+
148
+ if __name__ == "__main__":
149
+ main()
steer/config.py ADDED
@@ -0,0 +1,23 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Optional
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ class SteerConfig:
9
+ def __init__(self):
10
+ self.project_root = Path(os.getcwd())
11
+ self.steer_dir = self.project_root / ".steer"
12
+ self.log_file = self.steer_dir / "runs.jsonl"
13
+ self.rules_file = self.project_root / "steer_rules.yaml"
14
+ self.steer_dir.mkdir(parents=True, exist_ok=True)
15
+
16
+ # Keys
17
+ self.openai_api_key = os.getenv("OPENAI_API_KEY")
18
+ self.gemini_api_key = os.getenv("GEMINI_API_KEY") # <--- NEW
19
+
20
+ # Judge Defaults
21
+ self.judge_model = os.getenv("Steer_JUDGE_MODEL", "gemini/gemini-1.5-flash")
22
+
23
+ settings = SteerConfig()
steer/core.py ADDED
@@ -0,0 +1,125 @@
1
+ import functools
2
+ import inspect
3
+ from datetime import datetime, timezone
4
+ from typing import Callable, List, Any
5
+
6
+ from .schemas import Incident, TraceStep, TeachingOption
7
+ from .worker import get_worker
8
+ from .verifiers import BaseVerifier
9
+ from .storage import rulebook
10
+
11
+ class VerificationError(Exception):
12
+ def __init__(self, message, result):
13
+ super().__init__(message)
14
+ self.result = result
15
+
16
+ def capture(
17
+ name: str = "Agent Workflow",
18
+ verifiers: List[BaseVerifier] = None,
19
+ severity: str = "Medium",
20
+ tags: List[str] = None,
21
+ halt_on_failure: bool = True
22
+ ):
23
+ def decorator(func: Callable):
24
+ @functools.wraps(func)
25
+ def wrapper(*args, **kwargs):
26
+ start_time = datetime.now(timezone.utc)
27
+ current_agent = tags[0] if tags and len(tags) > 0 else "default_agent"
28
+
29
+ # 1. Dependency Injection
30
+ sig = inspect.signature(func)
31
+ if "steer_rules" in sig.parameters:
32
+ if "steer_rules" not in kwargs:
33
+ active_rules = rulebook.get_rules_text(current_agent)
34
+ kwargs["steer_rules"] = active_rules
35
+
36
+ # 2. Capture Input (Raw String)
37
+ display_input = "-"
38
+ if args:
39
+ display_input = str(args[0])
40
+ elif kwargs:
41
+ clean_kwargs = [str(v) for k, v in kwargs.items() if k != 'steer_rules']
42
+ display_input = ", ".join(clean_kwargs)
43
+
44
+ trace_steps: List[TraceStep] = []
45
+ trace_steps.append(TraceStep(type="user", title="User Input", content=display_input))
46
+ trace_steps.append(TraceStep(type="agent", title="Reasoning", content=f"Executing {func.__name__}..."))
47
+
48
+ # 3. Execution
49
+ error_msg = None
50
+ result = None
51
+ try:
52
+ result = func(*args, **kwargs)
53
+ display_output = str(result)
54
+ if isinstance(result, dict):
55
+ display_output = result.get("final_answer") or result.get("answer") or result.get("response") or str(result)
56
+ trace_steps.append(TraceStep(type="success", title="Output Generated", content=display_output))
57
+ except Exception as e:
58
+ error_msg = str(e)
59
+ trace_steps.append(TraceStep(type="error", title="Runtime Exception", content=f"❌ {error_msg}"))
60
+
61
+ # 4. Verification
62
+ detected_failure = None
63
+ verification_label = "Runtime Monitor"
64
+ smart_fixes = []
65
+
66
+ if verifiers and error_msg is None:
67
+ flat_inputs = {}
68
+ if kwargs: flat_inputs.update(kwargs)
69
+ flat_inputs['__active_rules__'] = rulebook.get_rules_text(current_agent)
70
+
71
+ for v in verifiers:
72
+ v_result = v.verify(flat_inputs, result)
73
+ if not v_result.passed:
74
+ # CRITICAL FIX: We set content=str(result) to show the raw bad output
75
+ trace_steps.append(TraceStep(
76
+ type="error",
77
+ title=f"BLOCKED: {v_result.reason}",
78
+ content=str(result)
79
+ ))
80
+ detected_failure = v_result
81
+ verification_label = v_result.verifier_name
82
+ smart_fixes = v_result.suggested_fixes
83
+ break
84
+
85
+ # 5. Logging
86
+ is_failure = error_msg is not None or detected_failure is not None
87
+
88
+ if is_failure:
89
+ log_status = "Active"
90
+ log_title = f"{verification_label} Failure" if detected_failure else "Runtime Error"
91
+ if not smart_fixes:
92
+ smart_fixes = [TeachingOption(title="Suppress", description="Ignore rule.", logic_change="None")]
93
+ else:
94
+ log_status = "Resolved"
95
+ log_title = "Execution Success"
96
+ smart_fixes = []
97
+
98
+ safe_args = [str(a) for a in args]
99
+ safe_kwargs = {k:str(v) for k,v in kwargs.items() if k != 'steer_rules'}
100
+
101
+ incident = Incident(
102
+ title=log_title,
103
+ agent_name=current_agent,
104
+ status=log_status,
105
+ detection_source="FAST_PATH",
106
+ detection_label=verification_label if detected_failure else "System",
107
+ severity=severity if is_failure else "Low",
108
+ timestamp=start_time,
109
+ trace=trace_steps,
110
+ raw_inputs={'args': safe_args, 'kwargs': safe_kwargs},
111
+ raw_outputs=str(result),
112
+ teaching_options=smart_fixes
113
+ )
114
+
115
+ get_worker().submit(incident.model_dump(mode='json'))
116
+
117
+ if detected_failure and halt_on_failure:
118
+ raise VerificationError(f"Blocked by {verification_label}: {detected_failure.reason}", result)
119
+
120
+ if error_msg:
121
+ raise Exception(error_msg)
122
+ return result
123
+
124
+ return wrapper
125
+ return decorator
steer/llm.py ADDED
@@ -0,0 +1,55 @@
1
+ import json
2
+ import litellm
3
+ from typing import Any, Dict
4
+ from .config import settings
5
+
6
+ litellm.suppress_instrumentation = True
7
+
8
+ class Judge:
9
+ @staticmethod
10
+ def is_configured() -> bool:
11
+ return bool(settings.openai_api_key or settings.gemini_api_key)
12
+
13
+ @staticmethod
14
+ def evaluate(system_prompt: str, user_context: str) -> Dict[str, Any]:
15
+ if not Judge.is_configured():
16
+ return {"passed": True, "reason": "Skipped: No Key"}
17
+
18
+ # Use Gemini Flash by default for speed
19
+ target_model = settings.judge_model
20
+ api_key = None
21
+
22
+ # Route key based on model
23
+ if "gemini" in target_model:
24
+ api_key = settings.gemini_api_key
25
+ # Ensure prefix exists
26
+ if not target_model.startswith("gemini/"):
27
+ target_model = f"gemini/{target_model}"
28
+ else:
29
+ api_key = settings.openai_api_key
30
+
31
+ try:
32
+ # print(f" [Judge] Thinking ({target_model})...")
33
+ response = litellm.completion(
34
+ model=target_model,
35
+ api_key=api_key,
36
+ messages=[
37
+ {"role": "system", "content": system_prompt},
38
+ {"role": "user", "content": user_context}
39
+ ],
40
+ response_format={"type": "json_object"},
41
+ temperature=0,
42
+ timeout=5 # Fast timeout
43
+ )
44
+
45
+ content = response.choices[0].message.content
46
+ # Gemini Markdown Cleanup
47
+ if "```" in content:
48
+ content = content.replace("```json", "").replace("```", "").strip()
49
+
50
+ return json.loads(content)
51
+
52
+ except Exception as e:
53
+ print(f" [Judge Error] {str(e)[:50]}...")
54
+ # Fail open on error so we don't crash the user's app
55
+ return {"passed": True, "reason": "Judge Error"}
steer/mock.py ADDED
@@ -0,0 +1,65 @@
1
+ import json
2
+ import time
3
+ import random
4
+
5
+ class MockLLM:
6
+ """
7
+ A Simulation Engine that mimics an LLM's behavior for Steer demos.
8
+ It reacts to specific keywords in the System Prompt to simulate "Learning".
9
+ """
10
+ @staticmethod
11
+ def call(system_prompt: str, user_prompt: str):
12
+ # Simulate network latency
13
+ time.sleep(0.3)
14
+
15
+ system_lower = system_prompt.lower()
16
+ user_lower = user_prompt.lower()
17
+
18
+ # --- DEMO 1: JSON STRUCTURE GUARD ---
19
+ if "profile" in user_lower or "u-8821" in user_lower:
20
+ # TRIGGER KEYWORDS
21
+ if any(k in system_lower for k in ["format critical", "valid json", "strict json", "no backticks"]):
22
+ return json.dumps({
23
+ "id": "u-8821",
24
+ "name": "Alice",
25
+ "role": "admin",
26
+ "status": "active"
27
+ }, indent=2)
28
+
29
+ # Default Failure
30
+ return """```json
31
+ {
32
+ "id": "u-8821",
33
+ "name": "Alice",
34
+ "role": "admin",
35
+ "status": "active"
36
+ }
37
+ ```"""
38
+
39
+ # --- DEMO 2: PRIVACY GUARD ---
40
+ if "ticket" in user_lower:
41
+ # TRIGGER KEYWORDS
42
+ if any(k in system_lower for k in ["security override", "redact", "pii"]):
43
+ return "I have contacted [REDACTED] regarding their refund request."
44
+
45
+ # Default Failure
46
+ return "I have contacted alice@example.com regarding their refund request."
47
+
48
+ # --- DEMO 3: LOGIC GUARD ---
49
+ if "weather" in user_lower or "springfield" in user_lower:
50
+ results = ["Springfield, IL", "Springfield, MA", "Springfield, MO", "Springfield, OR"]
51
+
52
+ # TRIGGER KEYWORDS (Fixed: Removed 'policy' to avoid false positives)
53
+ if any(k in system_lower for k in ["ask", "clarify", "multiple results"]):
54
+ return {
55
+ "message": "I found multiple Springfields. Which state do you mean?",
56
+ "results": results
57
+ }
58
+
59
+ # Default Failure
60
+ return {
61
+ "message": "The weather in Springfield, IL is 72°F.",
62
+ "results": results
63
+ }
64
+
65
+ return "I am a simulated model. I didn't understand the prompt context."
steer/schemas.py ADDED
@@ -0,0 +1,75 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Any, Dict, List, Optional, Literal
3
+ from pydantic import BaseModel, Field, ConfigDict
4
+ import uuid
5
+
6
+ # --- SHARED TYPES ---
7
+
8
+ class TeachingOption(BaseModel):
9
+ """A proposed fix for a failure."""
10
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
11
+ title: str
12
+ description: str
13
+ recommended: bool = False
14
+ logic_change: Optional[str] = None # The "Why?" tooltip
15
+
16
+ # --- LEGACY / INTERNAL TYPES (Needed for Verifiers) ---
17
+
18
+ class VerificationResult(BaseModel):
19
+ """The result of a single deterministic check."""
20
+ verifier_name: str
21
+ passed: bool
22
+ severity: str = "error"
23
+ reason: Optional[str] = None
24
+ # Allow verifiers to propose specific fixes
25
+ suggested_fixes: List[TeachingOption] = Field(default_factory=list)
26
+
27
+
28
+ # --- V4.1 ARCHITECTURE TYPES (Needed for Dashboard) ---
29
+
30
+ class TraceStep(BaseModel):
31
+ """Represents a single node in the visual timeline."""
32
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
33
+ type: Literal['user', 'agent', 'tool', 'error', 'success']
34
+ title: str
35
+ content: Optional[str] = None
36
+ metadata: Dict[str, str] = Field(default_factory=dict)
37
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
38
+
39
+ class DiagnosticMetrics(BaseModel):
40
+ """The scorecard (Health Metrics)."""
41
+ faithfulness: Optional[int] = None
42
+ relevance: Optional[int] = None
43
+ context_precision: Optional[int] = None
44
+
45
+ class Incident(BaseModel):
46
+ """
47
+ The Master Schema.
48
+ This maps 1:1 to the 'Incident' type in the Next.js Dashboard.
49
+ """
50
+ model_config = ConfigDict(arbitrary_types_allowed=True)
51
+
52
+ id: str = Field(default_factory=lambda: f"INC-{uuid.uuid4().hex[:6].upper()}")
53
+ title: str
54
+
55
+ # NEW: Track which agent this belongs to
56
+ agent_name: str = "default_agent"
57
+
58
+ status: Literal['Active', 'Resolved'] = 'Active'
59
+
60
+ # The "Badge" Logic
61
+ detection_source: Literal['FAST_PATH', 'SLOW_PATH'] = 'FAST_PATH'
62
+ detection_label: str # e.g. "Programmatic Verifier"
63
+
64
+ severity: Literal['High', 'Medium', 'Low'] = 'Medium'
65
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
66
+
67
+ metrics: Optional[DiagnosticMetrics] = None
68
+
69
+ # The Visuals
70
+ trace: List[TraceStep] = Field(default_factory=list)
71
+ teaching_options: List[TeachingOption] = Field(default_factory=list)
72
+
73
+ # Raw Metadata (hidden from UI but useful for debug)
74
+ raw_inputs: Dict[str, Any] = Field(default_factory=dict)
75
+ raw_outputs: Optional[Any] = None
steer/server.py ADDED
@@ -0,0 +1,122 @@
1
+ import json
2
+ import os
3
+ import yaml
4
+ from typing import List, Dict, Optional
5
+ from pathlib import Path
6
+ from fastapi import FastAPI, HTTPException
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.staticfiles import StaticFiles
9
+ from fastapi.responses import FileResponse
10
+ from pydantic import BaseModel
11
+
12
+ from .config import settings
13
+ from .storage import rulebook
14
+
15
+ app = FastAPI(title="Steer Mission Control")
16
+
17
+ # CORS (Useful if you ever develop frontend separately again)
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"],
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ # --- DATA MODELS ---
26
+
27
+ class TeachRequest(BaseModel):
28
+ agent_name: str = "default_agent"
29
+ rule_content: str
30
+ category: str = "general"
31
+ incident_id: Optional[str] = None
32
+
33
+ # --- API ROUTES ---
34
+
35
+ @app.get("/api/incidents")
36
+ def get_incidents():
37
+ if not settings.log_file.exists():
38
+ return {"incidents": []}
39
+
40
+ incidents = []
41
+ resolutions_path = settings.steer_dir / "resolutions.json"
42
+ resolved_ids = []
43
+ if resolutions_path.exists():
44
+ try:
45
+ resolved_ids = json.loads(resolutions_path.read_text())
46
+ except: pass
47
+
48
+ try:
49
+ with open(settings.log_file, "r") as f:
50
+ for line in f:
51
+ try:
52
+ inc = json.loads(line)
53
+ if inc.get("id") in resolved_ids:
54
+ inc["status"] = "Resolved"
55
+ incidents.append(inc)
56
+ except:
57
+ continue
58
+ except Exception as e:
59
+ print(f"Error reading logs: {e}")
60
+ return {"incidents": []}
61
+
62
+ return {"incidents": incidents[::-1]}
63
+
64
+ @app.get("/api/rules")
65
+ def get_rules():
66
+ try:
67
+ if not settings.rules_file.exists():
68
+ return {"rules": {}}
69
+ with open(settings.rules_file, "r") as f:
70
+ data = yaml.safe_load(f) or {}
71
+ return {"rules": data}
72
+ except Exception as e:
73
+ raise HTTPException(status_code=500, detail=str(e))
74
+
75
+ @app.post("/api/teach")
76
+ def teach_agent(payload: TeachRequest):
77
+ try:
78
+ # 1. Save Rule
79
+ rulebook.add_rule(
80
+ agent_name=payload.agent_name,
81
+ rule_content=payload.rule_content,
82
+ category=payload.category
83
+ )
84
+
85
+ # 2. Resolve Incident
86
+ if payload.incident_id:
87
+ resolutions_path = settings.steer_dir / "resolutions.json"
88
+ current_resolutions = []
89
+ if resolutions_path.exists():
90
+ try:
91
+ current_resolutions = json.loads(resolutions_path.read_text())
92
+ except: pass
93
+
94
+ if payload.incident_id not in current_resolutions:
95
+ current_resolutions.append(payload.incident_id)
96
+ with open(resolutions_path, "w") as f:
97
+ json.dump(current_resolutions, f, indent=2)
98
+
99
+ return {"success": True}
100
+ except Exception as e:
101
+ raise HTTPException(status_code=500, detail=str(e))
102
+
103
+ # --- STATIC FILE SERVING (The Bundle) ---
104
+
105
+ # Calculate path to the bundled UI folder
106
+ UI_DIR = Path(__file__).parent / "ui"
107
+
108
+ if UI_DIR.exists():
109
+ # 1. Serve Next.js static assets (_next folder)
110
+ app.mount("/_next", StaticFiles(directory=UI_DIR / "_next"), name="next_assets")
111
+
112
+ # 2. Catch-all for HTML (Single Page App routing)
113
+ # We explicitly match "/" to serve index.html
114
+ @app.get("/")
115
+ async def read_index():
116
+ return FileResponse(UI_DIR / "index.html")
117
+
118
+ # Optional: Handle favicon if it exists
119
+ if (UI_DIR / "favicon.ico").exists():
120
+ @app.get("/favicon.ico")
121
+ async def read_favicon():
122
+ return FileResponse(UI_DIR / "favicon.ico")