Spaces:
Running
Running
Download inference.py from RAc1928/second-brain-env: direct link, hf CLI and curl.
- Browser
- Download file 18.2 kB
-
https://huggingface.co/spaces/RAc1928/second-brain-env/resolve/main/inference.py
- Command line
-
hf download hf://spaces/RAc1928/second-brain-env/inference.py
-
curl -L -o inference.py https://huggingface.co/spaces/RAc1928/second-brain-env/resolve/main/inference.py
18.2 kB
| """ | |
| inference.py β Second Brain OpenEnv Baseline Inference Script | |
| Runs all 3 tasks against the environment and produces structured logs. | |
| Uses Groq API for LLM inference. | |
| """ | |
| import re | |
| import asyncio | |
| import json | |
| import os | |
| import sys | |
| import subprocess | |
| import time | |
| import textwrap | |
| from typing import List, Optional | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from openai import OpenAI | |
| from client import SecondBrainEnv | |
| from models import SecondBrainAction, ActionType | |
| # --------------------------------------------------------------------------- | |
| # Configuration | |
| # --------------------------------------------------------------------------- | |
| API_KEY = os.getenv("GROQ_API_KEY") or os.getenv("HF_TOKEN") or "" | |
| API_BASE_URL = os.getenv("API_BASE_URL", "https://api.groq.com/openai/v1") | |
| MODEL_NAME = os.getenv("MODEL_NAME", "llama-3.1-8b-instant") | |
| BENCHMARK = "second_brain_env" | |
| MAX_STEPS = 20 | |
| TEMPERATURE = 0.2 | |
| MAX_TOKENS = 512 | |
| SUCCESS_THRESHOLD = 0.5 | |
| TASK_PORTS = { | |
| "note_categorization": 8001, | |
| "memory_retrieval": 8002, | |
| "knowledge_synthesis": 8003, | |
| } | |
| TASKS = [ | |
| "note_categorization", | |
| "memory_retrieval", | |
| "knowledge_synthesis", | |
| ] | |
| # --------------------------------------------------------------------------- | |
| # Server management β auto-start local servers if not already running | |
| # --------------------------------------------------------------------------- | |
| _server_procs: List[subprocess.Popen] = [] | |
| def _check_server(port: int) -> bool: | |
| """Check if a server is already running on the given port.""" | |
| import socket | |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | |
| return s.connect_ex(("localhost", port)) == 0 | |
| def start_servers(): | |
| """Start uvicorn servers for each task if they aren't already running.""" | |
| env = os.environ.copy() | |
| env["PYTHONPATH"] = os.path.dirname(os.path.abspath(__file__)) | |
| for task_name, port in TASK_PORTS.items(): | |
| if _check_server(port): | |
| print(f"[INFO] Server already running on port {port} for {task_name}", flush=True) | |
| continue | |
| env_copy = env.copy() | |
| env_copy["TASK_NAME"] = task_name | |
| proc = subprocess.Popen( | |
| [sys.executable, "-m", "uvicorn", "server.app:app", | |
| "--host", "0.0.0.0", "--port", str(port)], | |
| env=env_copy, | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL, | |
| cwd=os.path.dirname(os.path.abspath(__file__)), | |
| ) | |
| _server_procs.append(proc) | |
| print(f"[INFO] Started server for {task_name} on port {port} (PID {proc.pid})", flush=True) | |
| # Wait for servers to be ready | |
| print("[INFO] Waiting for servers to start...", flush=True) | |
| for attempt in range(30): | |
| all_ready = all(_check_server(p) for p in TASK_PORTS.values()) | |
| if all_ready: | |
| print("[INFO] All servers ready.", flush=True) | |
| return | |
| time.sleep(1) | |
| print("[WARN] Some servers may not be ready after 30s.", flush=True) | |
| def stop_servers(): | |
| """Stop all servers started by this script.""" | |
| for proc in _server_procs: | |
| try: | |
| proc.terminate() | |
| proc.wait(timeout=5) | |
| except Exception: | |
| proc.kill() | |
| _server_procs.clear() | |
| # --------------------------------------------------------------------------- | |
| # Logging helpers β exact format required by competition | |
| # --------------------------------------------------------------------------- | |
| def log_start(task: str, env: str, model: str) -> None: | |
| print(f"[START] task={task} env={env} model={model}", flush=True) | |
| def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None: | |
| error_val = error if error else "null" | |
| done_val = str(done).lower() | |
| action_clean = action.replace("\n", " ").replace("\r", "")[:120] # type: ignore | |
| print( | |
| f"[STEP] step={step} action={action_clean} reward={reward:.2f} " | |
| f"done={done_val} error={error_val}", | |
| flush=True, | |
| ) | |
| def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None: | |
| rewards_str = ",".join(f"{r:.2f}" for r in rewards) | |
| print( | |
| f"[END] success={str(success).lower()} steps={steps} " | |
| f"score={score:.3f} rewards={rewards_str}", | |
| flush=True, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # System prompts | |
| # --------------------------------------------------------------------------- | |
| SYSTEM_PROMPTS = { | |
| "note_categorization": textwrap.dedent(""" | |
| You are a note classifier. Your ONLY job is to output a JSON object with | |
| the correct category for the note shown to you. | |
| Valid categories (pick EXACTLY one): | |
| action_item β note contains a task to DO: follow up, book, send, order, call, invoice | |
| work β professional meetings, standups, sprints, tech decisions (no action needed) | |
| personal β personal life, family, hobbies, travel, health, movies | |
| reference β articles, tips, cheatsheets, resources saved for later | |
| Decision rules (apply in order): | |
| 1. Contains "follow up", "book", "send", "order", "call", "invoice" β action_item | |
| 2. Mentions meetings, standup, sprint, backend, frontend, team, manager β work | |
| 3. Mentions family, hobby, movie, travel, health, weekend β personal | |
| 4. Everything else (articles, tips, guides, cheatsheets) β reference | |
| Output format β JSON and NOTHING else, no explanation, no preamble: | |
| {"action_type": "categorize", "content": "CATEGORY"} | |
| Example: | |
| Note: "Follow up with Alice about the contract." | |
| Output: {"action_type": "categorize", "content": "action_item"} | |
| """).strip(), | |
| "memory_retrieval": textwrap.dedent(""" | |
| You are a knowledge retrieval agent. | |
| Convert the question into 3-5 important keywords only. | |
| Focus on nouns: names, places, dates, topics. | |
| Do NOT use full sentences. | |
| ALWAYS return JSON only: | |
| {"action_type": "retrieve", "content": "keyword1 keyword2 keyword3"} | |
| """).strip(), | |
| "knowledge_synthesis": textwrap.dedent(""" | |
| You are a knowledge synthesis agent. | |
| === RETRIEVE action === | |
| Use 3-6 CONCRETE words that literally appear in the notes. | |
| Good examples: "sleep skipped meetings overwhelmed" | |
| "exercise stress caffeine headache" | |
| "Slack boundaries sprint async standup" | |
| "microservices architecture OKR Q2 Q3" | |
| "meditation walk lunch focus productivity" | |
| Bad examples (too abstract, won't match): | |
| "self-care patterns strategies connections" | |
| "health-related lifestyle changes habits" | |
| Each retrieve call MUST use DIFFERENT words from the previous call. | |
| {"action_type": "retrieve", "content": "concrete word1 word2 word3"} | |
| === SYNTHESIZE action === | |
| Write a detailed answer of at LEAST 60 words. | |
| - Address the CURRENT QUESTION directly. | |
| - Cover ALL themes from the question (stress, health, exercise, sleep, | |
| meetings, architecture, microservices, okr, async, meditation, | |
| boundaries, productivity β use whichever apply). | |
| - Connect causes to effects with specific examples from the notes. | |
| - Do NOT invent note IDs or facts not in the retrieved notes. | |
| {"action_type": "synthesize", "content": "60+ word answer covering all themes"} | |
| Return JSON only. No text outside the JSON. | |
| """).strip(), | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Per-question retrieval keyword hints | |
| # --------------------------------------------------------------------------- | |
| RETRIEVAL_HINTS = { | |
| "work stress notes": "overwhelmed meetings sleep skipped exercise", | |
| "patterns connect": "overwhelmed meetings sleep skipped exercise", | |
| "technical decisions": "microservices architecture OKR async standup", | |
| "q2/q3": "microservices architecture OKR async standup", | |
| "lifestyle changes": "meditation walk Slack sleep boundaries", | |
| "what actually worked": "meditation walk Slack sleep boundaries", | |
| "john": "john deadline april project", | |
| "weather service": "api rate limit weather calls", | |
| "visa": "visa appointment june documents", | |
| "architecture": "architecture microservices monolith auth Q3", | |
| "pydantic": "python pydantic dataclass validation", | |
| } | |
| def _get_hint(query: str) -> str: | |
| if not query: | |
| return "" | |
| q_lower = query.lower() | |
| for key, hint in RETRIEVAL_HINTS.items(): | |
| if key in q_lower: | |
| return hint | |
| return "" | |
| # --------------------------------------------------------------------------- | |
| # Agent action | |
| # --------------------------------------------------------------------------- | |
| def get_agent_action( | |
| client: OpenAI, | |
| task_name: str, | |
| obs: dict, | |
| step: int, | |
| history: List[str], | |
| force_synthesize: bool = False, | |
| ) -> SecondBrainAction: | |
| context_parts = [f"Step: {step}"] | |
| if task_name == "note_categorization": | |
| note = obs.get("current_note") or {} | |
| note_text = note.get("text", "") | |
| context_parts.append(f'Note: "{note_text}"') | |
| context_parts.append(f"Remaining notes: {obs.get('remaining_items', 0)}") | |
| else: | |
| query = obs.get("query", "") | |
| if query: | |
| context_parts.append(f"CURRENT QUESTION: {query}") | |
| if obs.get("retrieved_notes"): | |
| notes_text = "\n".join( | |
| f" [{n['id']}] {n['text']}" for n in obs["retrieved_notes"][:5] | |
| ) | |
| context_parts.append(f"Retrieved notes so far:\n{notes_text}") | |
| if obs.get("feedback"): | |
| context_parts.append(f"Feedback: {obs['feedback']}") | |
| context_parts.append(f"Remaining questions: {obs.get('remaining_items', 0)}") | |
| if history: | |
| context_parts.append("Recent actions:\n" + "\n".join(history[-2:])) # type: ignore | |
| if force_synthesize: | |
| context_parts.append( | |
| "\nINSTRUCTION: You MUST now SYNTHESIZE. Write at least 60 words. " | |
| "Use these theme words in your answer (where relevant): " | |
| "stress, health, exercise, sleep, meetings, architecture, microservices, " | |
| "okr, async, meditation, boundaries, productivity. " | |
| "Address the CURRENT QUESTION directly. Use action_type='synthesize'." | |
| ) | |
| else: | |
| hint = _get_hint(obs.get("query", "")) | |
| hint_text = f" Start with these concrete words: [{hint}]." if hint and step <= 2 else "" | |
| context_parts.append( | |
| f"\nINSTRUCTION: RETRIEVE notes using concrete words that appear in the notes.{hint_text} " | |
| "Use DIFFERENT keywords from any previous retrieve." | |
| ) | |
| user_prompt = "\n".join(context_parts) | |
| try: | |
| completion = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPTS[task_name]}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| temperature=TEMPERATURE, | |
| max_tokens=MAX_TOKENS, | |
| ) | |
| text = (completion.choices[0].message.content or "").strip() | |
| # Strip markdown code fences if present | |
| if "```" in text: | |
| parts = text.split("```") | |
| text = parts[1] if len(parts) > 1 else parts[0] | |
| if text.startswith("json"): | |
| text = text[4:] # type: ignore | |
| text = text.strip() | |
| # Strip control characters that break json.loads | |
| text = re.sub(r'[\x00-\x1f\x7f]', ' ', text) | |
| data = json.loads(text) | |
| action = SecondBrainAction( | |
| action_type=ActionType(data.get("action_type", "skip")), | |
| content=data.get("content", ""), | |
| note_id=data.get("note_id"), | |
| tags=data.get("tags"), | |
| ) | |
| except Exception as exc: | |
| print(f"[DEBUG] LLM parse error: {exc}", flush=True) | |
| fallbacks = { | |
| "note_categorization": SecondBrainAction( | |
| action_type=ActionType.categorize, | |
| content="work", | |
| ), | |
| "memory_retrieval": SecondBrainAction( | |
| action_type=ActionType.retrieve, | |
| content="meeting project deadline", | |
| ), | |
| "knowledge_synthesis": SecondBrainAction( | |
| action_type=ActionType.synthesize, | |
| content=( | |
| "The notes reveal a clear cycle: high meeting load causes skipped exercise and poor sleep, " | |
| "increasing stress further. Technical decisions for Q2/Q3 include microservices architecture, " | |
| "async standups, and performance OKRs β all reducing synchronous overhead. " | |
| "Effective lifestyle changes include meditation, lunch walks, stopping Slack after 8pm, " | |
| "and no-meeting Fridays β each improved sleep, focus, and productivity immediately." | |
| ), | |
| ), | |
| } | |
| action = fallbacks[task_name] | |
| # --- Hard-enforce correct action_type per task --- | |
| if task_name == "memory_retrieval": | |
| action.action_type = ActionType.retrieve | |
| if task_name == "knowledge_synthesis": | |
| if force_synthesize: | |
| action.action_type = ActionType.synthesize | |
| else: | |
| action.action_type = ActionType.retrieve | |
| # Empty retrieve content is useless β give it a default | |
| if action.action_type == ActionType.retrieve and not action.content.strip(): | |
| action.content = "meeting project deadline task" | |
| return action | |
| # --------------------------------------------------------------------------- | |
| # Run one task | |
| # --------------------------------------------------------------------------- | |
| async def run_task(client: OpenAI, task_name: str) -> float: | |
| port = TASK_PORTS[task_name] | |
| env = SecondBrainEnv(base_url=f"http://localhost:{port}", task_name=task_name) | |
| history: List[str] = [] | |
| rewards: List[float] = [] | |
| steps_taken = 0 | |
| score = 0.0 | |
| success = False | |
| log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME) | |
| try: | |
| result = await env.reset() | |
| obs: dict = result.observation.model_dump() if hasattr(result.observation, "model_dump") else dict(result.observation) # type: ignore | |
| print( | |
| f"[DEBUG] Server task: {obs.get('task_name')} " | |
| f"| query: {obs.get('query')} " | |
| f"| note: {obs.get('current_note', {}).get('id') if obs.get('current_note') else None}", | |
| flush=True, | |
| ) | |
| retrieves_this_question = 0 | |
| RETRIEVES_BEFORE_SYNTH = 3 | |
| for step in range(1, MAX_STEPS + 1): | |
| if result.done: | |
| break | |
| force_synthesize = ( | |
| task_name == "knowledge_synthesis" | |
| and retrieves_this_question >= RETRIEVES_BEFORE_SYNTH | |
| ) | |
| action = get_agent_action(client, task_name, obs, step, history, force_synthesize) | |
| action_str = f"{action.action_type}({action.content[:80]})" | |
| try: | |
| result = await env.step(action) | |
| reward = float(getattr(result, "reward", 0.0)) | |
| obs: dict = result.observation.model_dump() if hasattr(result.observation, "model_dump") else dict(result.observation) # type: ignore | |
| done = bool(result.done) | |
| error = None | |
| if obs.get("retrieved_notes"): | |
| for n in obs["retrieved_notes"]: | |
| print(f" [RETRIEVED] {n['id']}: {n['text'][:70]}", flush=True) | |
| if task_name == "knowledge_synthesis": | |
| if action.action_type == ActionType.synthesize: | |
| retrieves_this_question = 0 | |
| else: | |
| retrieves_this_question += 1 | |
| except Exception as e: | |
| reward = 0.0 | |
| done = True | |
| error = str(e)[:80] # type: ignore | |
| rewards.append(reward) | |
| steps_taken = step | |
| log_step(step=step, action=action_str, reward=reward, done=done, error=error) | |
| history.append(f"Step {step}: {action_str} β reward {reward:+.2f}") | |
| if done: | |
| break | |
| score = float(obs.get("score", 0.0)) | |
| score = max(0.0, min(1.0, score)) | |
| success = score >= SUCCESS_THRESHOLD | |
| except Exception as e: | |
| print(f"[DEBUG] Episode error: {e}", flush=True) | |
| finally: | |
| try: | |
| await env.close() | |
| except Exception: | |
| pass | |
| log_end(success=success, steps=steps_taken, score=score, rewards=rewards) | |
| return score | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| async def main() -> None: | |
| client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY) | |
| all_scores = {} | |
| for task_name in TASKS: | |
| print(f"\n{'='*60}", flush=True) | |
| print(f"Running task: {task_name}", flush=True) | |
| print(f"{'='*60}", flush=True) | |
| score = await run_task(client, task_name) | |
| all_scores[task_name] = score | |
| print(f"\n{'='*60}", flush=True) | |
| print("BASELINE SCORES SUMMARY", flush=True) | |
| print(f"{'='*60}", flush=True) | |
| for task, score in all_scores.items(): | |
| status = "PASS" if score >= SUCCESS_THRESHOLD else "FAIL" | |
| print(f" [{status}] {task}: {score:.3f}", flush=True) | |
| overall = sum(all_scores.values()) / len(all_scores) | |
| print(f"\n Overall average: {overall:.3f}", flush=True) | |
| print(f"{'='*60}", flush=True) | |
| if __name__ == "__main__": | |
| # Auto-start servers if not running | |
| start_servers() | |
| try: | |
| asyncio.run(main()) | |
| finally: | |
| stop_servers() |