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.
- steer/__init__.py +13 -0
- steer/cli.py +149 -0
- steer/config.py +23 -0
- steer/core.py +125 -0
- steer/llm.py +55 -0
- steer/mock.py +65 -0
- steer/schemas.py +75 -0
- steer/server.py +122 -0
- steer/storage.py +76 -0
- steer/ui/404.html +1 -0
- steer/ui/__next.__PAGE__.txt +9 -0
- steer/ui/__next._full.txt +20 -0
- steer/ui/__next._head.txt +8 -0
- steer/ui/__next._index.txt +6 -0
- steer/ui/__next._tree.txt +3 -0
- steer/ui/_next/static/chunks/0d03996d4dc2f4a9.js +1 -0
- steer/ui/_next/static/chunks/42879de7b8087bc9.js +1 -0
- steer/ui/_next/static/chunks/6d94cd2de0f5bc76.js +1 -0
- steer/ui/_next/static/chunks/752bea8c8f15cedd.js +2 -0
- steer/ui/_next/static/chunks/85e2cd8235bb75d4.css +2 -0
- steer/ui/_next/static/chunks/940e70d544422cd1.js +1 -0
- steer/ui/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- steer/ui/_next/static/chunks/d217698e32abd0dc.js +4 -0
- steer/ui/_next/static/chunks/turbopack-0d14a708cd8eabb2.js +3 -0
- steer/ui/_next/static/cxvsdwfl0GLod-qFpT8EQ/_buildManifest.js +11 -0
- steer/ui/_next/static/cxvsdwfl0GLod-qFpT8EQ/_clientMiddlewareManifest.json +1 -0
- steer/ui/_next/static/cxvsdwfl0GLod-qFpT8EQ/_ssgManifest.js +1 -0
- steer/ui/_next/static/media/1bffadaabf893a1e-s.7cd81963.woff2 +0 -0
- steer/ui/_next/static/media/2bbe8d2671613f1f-s.76dcb0b2.woff2 +0 -0
- steer/ui/_next/static/media/2c55a0e60120577a-s.2a48534a.woff2 +0 -0
- steer/ui/_next/static/media/5476f68d60460930-s.c995e352.woff2 +0 -0
- steer/ui/_next/static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2 +0 -0
- steer/ui/_next/static/media/9c72aa0f40e4eef8-s.18a48cbc.woff2 +0 -0
- steer/ui/_next/static/media/ad66f9afd8947f86-s.7a40eb73.woff2 +0 -0
- steer/ui/_next/static/media/favicon.0b3bf435.ico +0 -0
- steer/ui/_not-found/__next._full.txt +15 -0
- steer/ui/_not-found/__next._head.txt +8 -0
- steer/ui/_not-found/__next._index.txt +6 -0
- steer/ui/_not-found/__next._not-found.__PAGE__.txt +5 -0
- steer/ui/_not-found/__next._not-found.txt +4 -0
- steer/ui/_not-found/__next._tree.txt +2 -0
- steer/ui/_not-found.html +1 -0
- steer/ui/_not-found.txt +15 -0
- steer/ui/favicon.ico +0 -0
- steer/ui/file.svg +1 -0
- steer/ui/globe.svg +1 -0
- steer/ui/index.html +1 -0
- steer/ui/index.txt +20 -0
- steer/ui/next.svg +1 -0
- steer/ui/vercel.svg +1 -0
- steer/ui/window.svg +1 -0
- steer/utils.py +28 -0
- steer/verifiers.py +159 -0
- steer/worker.py +71 -0
- steer_sdk-0.1.8.dist-info/METADATA +209 -0
- steer_sdk-0.1.8.dist-info/RECORD +58 -0
- steer_sdk-0.1.8.dist-info/WHEEL +4 -0
- 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")
|