Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 51 additions & 10 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Literal

from fastapi import FastAPI, HTTPException, Query, Response, WebSocket
from fastapi import FastAPI, HTTPException, Query, Request, Response, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
Expand Down Expand Up @@ -80,6 +81,30 @@ def safely(action):
raise HTTPException(409, str(error))


_LOCALE_PATTERN = re.compile(r"^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$")


def request_locale(request: Request) -> str | None:
"""Return the browser's highest-priority valid ``Accept-Language`` locale."""
choices: list[tuple[float, int, str]] = []
for index, item in enumerate(request.headers.get("accept-language", "").split(",")):
value, *parameters = item.strip().split(";")
if not _LOCALE_PATTERN.fullmatch(value):
continue
quality = 1.0
for parameter in parameters:
name, separator, raw_value = parameter.strip().partition("=")
if name.lower() != "q" or not separator:
continue
try:
quality = float(raw_value)
except ValueError:
quality = 0.0
if quality > 0:
choices.append((quality, index, value))
return max(choices, default=(0.0, 0, ""), key=lambda item: (item[0], -item[1]))[2] or None


def paginated(values: list, response: Response, page: int, per_page: int) -> list:
"""Use GitLab-compatible pagination headers for collection endpoints."""
total = len(values)
Expand Down Expand Up @@ -358,15 +383,23 @@ class BuildRunRequest(BaseModel):


@app.post("/api/builds/{build_id}/runs")
def invoke_build(build_id: str, values: BuildRunRequest | None = None):
def invoke_build(build_id: str, request: Request, values: BuildRunRequest | None = None):
return safely(
lambda: store.invoke_remote_build(build_id, output_locale=values.output_locale if values else None)
lambda: store.invoke_remote_build(
build_id,
output_locale=(request_locale(request) or (values.output_locale if values else None)),
)
)


@app.post("/api/builds/{build_id}/tests")
def test_build(build_id: str, values: BuildRunRequest | None = None):
return safely(lambda: store.test_build(build_id, output_locale=values.output_locale if values else None))
def test_build(build_id: str, request: Request, values: BuildRunRequest | None = None):
return safely(
lambda: store.test_build(
build_id,
output_locale=(request_locale(request) or (values.output_locale if values else None)),
)
)


@app.get("/api/build-tests/{session_id}")
Expand Down Expand Up @@ -638,8 +671,10 @@ def list_project_pipelines(
status_code=201,
summary="Start a project pipeline",
)
def create_project_pipeline(project_id: str, values: PipelineCreate):
return safely(lambda: store.invoke_remote_build(project_id, values.execution_mode))
def create_project_pipeline(project_id: str, values: PipelineCreate, request: Request):
return safely(
lambda: store.invoke_remote_build(project_id, values.execution_mode, request_locale(request))
)


@app.get("/api/v1/quick-starts", tags=["Quick starts"], operation_id="listQuickStarts")
Expand Down Expand Up @@ -1010,7 +1045,7 @@ class CycleAnalysisRequest(BaseModel):


@app.post("/api/cycle-improvements/analyze")
def analyze_cycle(values: CycleAnalysisRequest):
def analyze_cycle(values: CycleAnalysisRequest, request: Request):
"""Use the configured system AI to diagnose a build's PDCA loop."""
profile_name = store.application_settings()["chat_model_profile_name"]
if not profile_name:
Expand Down Expand Up @@ -1043,7 +1078,11 @@ def analyze_cycle(values: CycleAnalysisRequest):
"not a single iteration. Identify evidence of plan, do, check, and act; score trends, "
"repeated proposals, and whether accepted work was verified. Recommend only operating-cycle "
"changes (runner, workflow, tests, supervisor prompt, or cadence). Respond only in "
+ (f"Use BCP 47 locale '{values.locale}'. " if values.locale else "")
+ (
f"Use BCP 47 locale '{request_locale(request) or values.locale}'. "
if request_locale(request) or values.locale
else ""
)
+ "Respond in concise Markdown with headings for Health, Evidence, Bottleneck, and Recommended next action.\n\n"
+ json.dumps(context, ensure_ascii=False, default=str)
)
Expand All @@ -1056,7 +1095,7 @@ def analyze_cycle(values: CycleAnalysisRequest):


@app.post("/api/chat")
def chat(values: ChatMessage):
def chat(values: ChatMessage, request: Request):
profile_name = store.application_settings()["chat_model_profile_name"]
if not profile_name:
raise HTTPException(409, "Select an AI model profile for the chat assistant in Settings.")
Expand All @@ -1070,6 +1109,8 @@ def chat(values: ChatMessage):
"The local OpenAPI contract is available at /api/openapi.json; use it as the source of truth "
"when explaining API endpoints, parameters, and response shapes.\n"
)
if locale := request_locale(request):
prompt += f"Respond in BCP 47 locale '{locale}'.\n"
if history:
prompt += f"Conversation so far:\n{history}\n\n"
prompt += f"User: {values.content}\nAssistant:"
Expand Down
2 changes: 1 addition & 1 deletion backend/app/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def _application_data_dir() -> Path:
\"improvements\": [{\"title\":\"string\",\"status\":\"proposed|adopted|rejected\",\"rationale\":\"string\",\"acceptanceEvidence\":\"string\"}],
\"reported_issues\": [{\"title\":\"string\",\"severity\":\"low|medium|high|critical\",\"evidence\":\"string\",\"reproduction\":\"string\",\"status\":\"open|acknowledged|resolved\"}]
}
For an evaluated AI, include behavior_trace and fill every field. This is an evidence-backed persona journey, not the evaluator's procedure and not hidden reasoning. Keep the visible journey concise: persona_goal is the persona's stable, first-person wish in one sentence; current_action is a first-person sentence describing the one meaningful action the persona took in this iteration; decision is the persona's first-person judgment or resulting choice from that action; next_action is the one specific, safe next action in first person. Do not describe navigation, waits, screenshots, generic control inspection, or other repeated mechanics in any visible journey field. evidence is a concise source-backed factual record for the evidence drawer, not a visible journey item. Use the persona's wording where useful, but do not invent motives, feelings, beliefs, or facts beyond the declared persona and observed evidence. Do not reveal hidden reasoning or evaluator chain-of-thought. Do not include behavior_trace for non-AI targets. behavior_summary is deprecated and should be omitted. Always include both array keys, using empty arrays when there are no items."""
For an evaluated AI, include behavior_trace and fill every field. This is an evidence-backed persona journey, not the evaluator's procedure and not hidden reasoning. Keep the visible journey concise and written from the persona's perspective: persona_goal is the persona's stable wish; current_action is the one meaningful action taken in this iteration; decision is the resulting judgment or choice; next_action is the one specific, safe next action. Write each field as one to three natural sentences in the selected output language. Follow that language's normal grammar, ellipsis, and point of view; do not mechanically repeat a subject or pronoun across fields. Do not describe navigation, waits, screenshots, generic control inspection, or other repeated mechanics in any visible journey field. evidence is a concise source-backed factual record for the evidence drawer, not a visible journey item. Use the persona's wording where useful, but do not invent motives, feelings, beliefs, or facts beyond the declared persona and observed evidence. Do not reveal hidden reasoning or evaluator chain-of-thought. Do not include behavior_trace for non-AI targets. behavior_summary is deprecated and should be omitted. Always include both array keys, using empty arrays when there are no items."""
LEGACY_OPERATIONAL_MANAGER_PROMPT = """You are an approval-first operations manager for recurring AI evaluations.
Preserve the task safety boundary, collect observable evidence, and never
claim success without stated acceptance evidence. Escalate required approvals
Expand Down
12 changes: 12 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from app.models import Run, Step, Workflow
from fastapi.testclient import TestClient
from orbit_sdk import RunnerContext
from starlette.requests import Request


def test_health_is_available():
Expand All @@ -23,6 +24,17 @@ def test_health_is_available():
assert response.json() == {"status": "ok"}


def test_request_locale_prefers_the_browser_accept_language_priority():
request = Request(
{
"type": "http",
"headers": [(b"accept-language", b"ja;q=0.6, ko-KR;q=0.9, en;q=0.8")],
}
)

assert main_module.request_locale(request) == "ko-KR"


def test_generated_sdk_docs_are_served_from_the_local_app(tmp_path, monkeypatch):
docs = tmp_path / "site" / "sdk"
docs.mkdir(parents=True)
Expand Down
Loading