Spaces:
Sleeping
Sleeping
| # ============================================================================ | |
| # agent_workflow.py — Workflow backend (fixed 2-step prompt chain) | |
| # ============================================================================ | |
| """Deterministic two-step prompt chain backend. | |
| Purpose | |
| ------- | |
| The simplest possible backend in the workbench: a hand-written, two-step | |
| prompt chain (clarify -> answer). No tools, no agent loop, no framework. | |
| The developer (not the model) decides what runs and in what order, so this | |
| file is the baseline students can compare every other backend against. | |
| Inputs | |
| ------ | |
| api_key : str — provider API key (may be empty when env var is set). | |
| user_message : str — the researcher's chat message. | |
| provider : str — one of providers.LLM_PROVIDERS keys. Default Mistral. | |
| Outputs | |
| ------- | |
| run(...) returns {"reply": str, "steps": list[dict], "extracted": dict} | |
| matching the workbench's universal backend contract. | |
| Side effects | |
| ------------ | |
| Two LLM completion calls per run() invocation. No disk I/O, no network | |
| state mutation beyond the LLM provider's own request log. | |
| Contract | |
| -------- | |
| BACKEND_NAME, get_client, run, build_code_snippets. | |
| """ | |
| from __future__ import annotations | |
| from typing import Any | |
| from parameters import MAX_TOKENS, TEMPERATURE | |
| from prompts import WORKFLOW_STEP1_CLARIFY, WORKFLOW_STEP2_ANSWER | |
| import providers | |
| BACKEND_NAME: str = "Workflow" | |
| # ---------------------------------------------------------------- | |
| # PUBLIC: client factory | |
| # ---------------------------------------------------------------- | |
| def get_client(api_key: str, provider: str = "Mistral") -> Any: | |
| """Build a provider-agnostic LLM client. | |
| All adapter logic lives in providers.py; this is a one-line forwarder so | |
| app.py never imports a specific SDK directly. | |
| Args: | |
| api_key: Provider API key. Empty string means "fall back to env var". | |
| provider: Name from providers.LLM_PROVIDERS. | |
| Returns: | |
| An object exposing ``client.chat.complete(model, messages, ...)`` | |
| whose response shape matches Mistral 1.x SDK. | |
| Raises: | |
| ValueError: when ``provider`` is not registered. | |
| Example: | |
| >>> client = get_client("", provider="Mistral") | |
| """ | |
| return providers.get_llm_client(provider, api_key) | |
| # ---------------------------------------------------------------- | |
| # INTERNAL: single LLM call, provider-agnostic | |
| # ---------------------------------------------------------------- | |
| def _call_llm(client: Any, system_prompt: str, user_text: str, provider: str) -> str: | |
| """One round-trip to the LLM with a system+user pair. | |
| Args: | |
| client: Object returned from get_client(). | |
| system_prompt: System role message (instruction). | |
| user_text: User role message (content to act on). | |
| provider: Provider name; used to pick the default model. | |
| Returns: | |
| The assistant's text content; empty string if the model produced none. | |
| Raises: | |
| RuntimeError: if the provider client raises during completion. The | |
| original exception's message is included so the caller can log it. | |
| """ | |
| try: | |
| response = client.chat.complete( | |
| model=providers.get_llm_model(provider), | |
| temperature=TEMPERATURE, | |
| max_tokens=MAX_TOKENS, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_text}, | |
| ], | |
| ) | |
| except Exception as exc: # narrow: re-raise with workbench framing | |
| raise RuntimeError( | |
| f"Workflow LLM call failed (provider={provider}): {exc}" | |
| ) from exc | |
| return response.choices[0].message.content or "" | |
| # ---------------------------------------------------------------- | |
| # Step pipeline — declared as data, executed as data. | |
| # Each entry is (step_no, label, system_prompt). The body of run() is | |
| # a list comprehension over this table — no if/elif, no per-step glue. | |
| # ---------------------------------------------------------------- | |
| _STEPS: list[tuple[int, str, str]] = [ | |
| (1, "clarify", WORKFLOW_STEP1_CLARIFY), | |
| (2, "answer", WORKFLOW_STEP2_ANSWER), | |
| ] | |
| # ---------------------------------------------------------------- | |
| # PUBLIC: run | |
| # ---------------------------------------------------------------- | |
| def run(client: Any, user_message: str, provider: str = "Mistral") -> dict[str, Any]: | |
| """Execute the fixed clarify -> answer chain. | |
| Each step's output becomes the next step's user input. The final step's | |
| output is the user-facing reply. Both steps are recorded in the step log | |
| so the UI can render them in the Process tab. | |
| Args: | |
| client: From get_client(). | |
| user_message: Researcher's chat message. | |
| provider: Provider name; passed through to _call_llm. | |
| Returns: | |
| Dict with keys ``reply`` (str), ``steps`` (list), ``extracted`` (dict). | |
| Raises: | |
| RuntimeError: bubbled up from _call_llm if the LLM call fails. | |
| Example: | |
| >>> result = run(client, "what is grounded theory?") | |
| >>> result["reply"] | |
| 'Grounded theory is...' | |
| """ | |
| steps: list[dict[str, Any]] = [] | |
| current_input: str = user_message | |
| # Declarative loop: walk the table, no per-step special-casing. | |
| for step_no, label, system_prompt in _STEPS: | |
| output = _call_llm(client, system_prompt, current_input, provider) | |
| steps.append({ | |
| "step": step_no, | |
| "type": "llm_call", | |
| "tool": label, | |
| "args": current_input, | |
| "result": output, | |
| }) | |
| current_input = output | |
| return { | |
| "reply": current_input, | |
| "steps": steps, | |
| "extracted": {"clarified_question": steps[0]["result"] if steps else ""}, | |
| } | |
| # ---------------------------------------------------------------- | |
| # PUBLIC: build_code_snippets | |
| # ---------------------------------------------------------------- | |
| def build_code_snippets(user_message: str, steps: list[dict[str, Any]]) -> str: | |
| """Render an annotated code snippet of what this backend just did. | |
| Args: | |
| user_message: The original researcher message. | |
| steps: The step log from run(). | |
| Returns: | |
| A multi-line string the UI's Code tab displays verbatim. | |
| """ | |
| header = [ | |
| "# Backend: Workflow", | |
| "# Fixed 2-step prompt chain (clarify -> answer). No tools, no framework.", | |
| f"# User message: {user_message}", | |
| "", | |
| "step1 = client.chat.complete(", | |
| " model=MODEL,", | |
| " messages=[", | |
| " {'role': 'system', 'content': WORKFLOW_STEP1_CLARIFY},", | |
| f" {{'role': 'user', 'content': {user_message!r}}},", | |
| " ],", | |
| ").choices[0].message", | |
| "clarified = step1.content", | |
| "", | |
| "step2 = client.chat.complete(", | |
| " model=MODEL,", | |
| " messages=[", | |
| " {'role': 'system', 'content': WORKFLOW_STEP2_ANSWER},", | |
| " {'role': 'user', 'content': clarified},", | |
| " ],", | |
| ").choices[0].message", | |
| "answer = step2.content", | |
| "", | |
| "# ---------- actual step log ----------", | |
| ] | |
| log_lines = [ | |
| line | |
| for s in steps | |
| for line in ( | |
| f"# Step {s['step']} [{s['type']}] {s['tool']}", | |
| f"# input: {s['args']!r}", | |
| f"# output: {s['result']!r}", | |
| ) | |
| ] | |
| return "\n".join(header + log_lines) | |