ticket #7 attempt 1

This commit is contained in:
Typosaurus 2026-07-19 19:11:40 +00:00
parent 818568c609
commit 0199a03a85
10 changed files with 73 additions and 8 deletions

1
.dpc/verify_cache.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -227,7 +227,8 @@ def _session_context(request: Request, uid: str, job: dict, session: dict) -> di
user = get_current_user(request) user = get_current_user(request)
viewer_is_admin = is_admin(user) viewer_is_admin = is_admin(user)
done = bool(report) or job.get("status") == queue.DONE done = bool(report) or job.get("status") == queue.DONE
return { cost_usd = report.get("cost_usd", 0.0) if report else 0.0
ctx = {
"uid": uid, "uid": uid,
"status": queue.DONE if done else job.get("status", ""), "status": queue.DONE if done else job.get("status", ""),
"query": report.get("query") or session.get("query"), "query": report.get("query") or session.get("query"),
@ -247,6 +248,7 @@ def _session_context(request: Request, uid: str, job: dict, session: dict) -> di
"export_md_url": f"/tools/deepsearch/{uid}/export.md" if done else None, "export_md_url": f"/tools/deepsearch/{uid}/export.md" if done else None,
"export_json_url": f"/tools/deepsearch/{uid}/export.json" if done else None, "export_json_url": f"/tools/deepsearch/{uid}/export.json" if done else None,
"export_pdf_url": f"/tools/deepsearch/{uid}/export.pdf" if done else None, "export_pdf_url": f"/tools/deepsearch/{uid}/export.pdf" if done else None,
"cost_usd": cost_usd if viewer_is_admin else None,
"viewer_is_admin": viewer_is_admin, "viewer_is_admin": viewer_is_admin,
"viewer_owns": _owns(request, session), "viewer_owns": _owns(request, session),
"created_at": session.get("created_at"), "created_at": session.get("created_at"),
@ -255,6 +257,7 @@ def _session_context(request: Request, uid: str, job: dict, session: dict) -> di
"user": user, "user": user,
"meta_robots": "noindex,nofollow", "meta_robots": "noindex,nofollow",
} }
return ctx
@router.get("/{uid}/session") @router.get("/{uid}/session")
async def deepsearch_session(request: Request, uid: str): async def deepsearch_session(request: Request, uid: str):

View File

@ -136,6 +136,7 @@ class DeepsearchSessionOut(_Out):
export_md_url: Optional[str] = None export_md_url: Optional[str] = None
export_json_url: Optional[str] = None export_json_url: Optional[str] = None
export_pdf_url: Optional[str] = None export_pdf_url: Optional[str] = None
cost_usd: Optional[float] = None
viewer_is_admin: bool = False viewer_is_admin: bool = False
viewer_owns: bool = False viewer_owns: bool = False
created_at: Optional[str] = None created_at: Optional[str] = None

View File

@ -7,6 +7,7 @@ import time
from devplacepy import stealth from devplacepy import stealth
from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL
from devplacepy.services.openai_gateway.usage import parse_usage_headers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -41,7 +42,10 @@ async def request_completion(
if response.status_code >= 400: if response.status_code >= 400:
raise RuntimeError(f"chat gateway returned {response.status_code}") raise RuntimeError(f"chat gateway returned {response.status_code}")
data: dict = response.json() data: dict = response.json()
return data, data.get("usage") or {}, elapsed_ms cost_info = parse_usage_headers(response.headers) or {}
usage = data.get("usage") or {}
usage["cost_usd"] = cost_info.get("cost_usd", 0.0)
return data, usage, elapsed_ms
async def complete_chat( async def complete_chat(

View File

@ -5,10 +5,11 @@ from __future__ import annotations
import json import json
import logging import logging
import re import re
from typing import Callable from typing import Callable, Tuple
from devplacepy import stealth from devplacepy import stealth
from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL
from devplacepy.services.openai_gateway.usage import parse_usage_headers
from .phases import PHASE_PLANNING from .phases import PHASE_PLANNING
@ -61,7 +62,7 @@ def _noop(frame: dict) -> None:
async def plan_queries( async def plan_queries(
query: str, api_key: str, emit: Callable[[dict], None] = _noop query: str, api_key: str, emit: Callable[[dict], None] = _noop
) -> list[str]: ) -> Tuple[list[str], float]:
emit( emit(
{ {
"type": "substep", "type": "substep",
@ -89,6 +90,8 @@ async def plan_queries(
) )
if response.status_code >= 400: if response.status_code >= 400:
raise RuntimeError(f"planner gateway returned {response.status_code}") raise RuntimeError(f"planner gateway returned {response.status_code}")
cost_info = parse_usage_headers(response.headers) or {}
planner_cost = cost_info.get("cost_usd", 0.0)
data = response.json() data = response.json()
content = ( content = (
(data.get("choices") or [{}])[0].get("message", {}).get("content") or "" (data.get("choices") or [{}])[0].get("message", {}).get("content") or ""
@ -102,7 +105,7 @@ async def plan_queries(
"message": f"Planned {len(parsed)} search angles", "message": f"Planned {len(parsed)} search angles",
} }
) )
return parsed return parsed, planner_cost
except Exception as exc: except Exception as exc:
logger.warning("deepsearch query planner failed, using fallback: %s", exc) logger.warning("deepsearch query planner failed, using fallback: %s", exc)
fallback = _fallback(query) fallback = _fallback(query)
@ -113,4 +116,4 @@ async def plan_queries(
"message": f"Planner unavailable, using {len(fallback)} heuristic angles", "message": f"Planner unavailable, using {len(fallback)} heuristic angles",
} }
) )
return fallback return fallback, 0.0

View File

@ -40,6 +40,7 @@ class Orchestration:
source_diversity: float = 0.0 source_diversity: float = 0.0
score: int = 0 score: int = 0
synthesis: str = "agents" synthesis: str = "agents"
total_cost_usd: float = 0.0
def _domain(url: str) -> str: def _domain(url: str) -> str:
@ -165,6 +166,7 @@ async def _complete(
"tokens_in": int(raw_usage.get("prompt_tokens") or prompt_chars // 4), "tokens_in": int(raw_usage.get("prompt_tokens") or prompt_chars // 4),
"tokens_out": int(raw_usage.get("completion_tokens") or len(text) // 4), "tokens_out": int(raw_usage.get("completion_tokens") or len(text) // 4),
"elapsed_ms": elapsed_ms, "elapsed_ms": elapsed_ms,
"cost_usd": float(raw_usage.get("cost_usd", 0.0)),
} }
return text, usage return text, usage
@ -296,6 +298,7 @@ def _agent_done(emit: Callable[[dict], None], agent: str, usage: dict) -> None:
"elapsed_ms": usage.get("elapsed_ms", 0), "elapsed_ms": usage.get("elapsed_ms", 0),
"tokens_in": usage.get("tokens_in", 0), "tokens_in": usage.get("tokens_in", 0),
"tokens_out": usage.get("tokens_out", 0), "tokens_out": usage.get("tokens_out", 0),
"cost_usd": usage.get("cost_usd", 0.0),
} }
) )
@ -346,6 +349,7 @@ async def orchestrate(
emit: Callable[[dict], None], emit: Callable[[dict], None],
store=None, store=None,
queries: list[str] | None = None, queries: list[str] | None = None,
planner_cost_usd: float = 0.0,
) -> Orchestration: ) -> Orchestration:
diversity = source_diversity(pages) diversity = source_diversity(pages)
if not pages: if not pages:
@ -418,6 +422,12 @@ async def orchestrate(
score = int( score = int(
min(SCORE_MAX, (confidence * 0.5 + diversity * 0.3 + coverage * 0.2) * SCORE_MAX) min(SCORE_MAX, (confidence * 0.5 + diversity * 0.3 + coverage * 0.2) * SCORE_MAX)
) )
total_cost = (
planner_cost_usd
+ float(summary_usage.get("cost_usd", 0.0))
+ float(findings_usage.get("cost_usd", 0.0))
+ float(linker_usage.get("cost_usd", 0.0))
)
return Orchestration( return Orchestration(
summary=summary, summary=summary,
findings=findings, findings=findings,
@ -425,6 +435,7 @@ async def orchestrate(
source_diversity=diversity, source_diversity=diversity,
score=score, score=score,
synthesis="agents", synthesis="agents",
total_cost_usd=total_cost,
) )
except Exception as exc: except Exception as exc:
logger.warning("deepsearch orchestration failed, using heuristic: %s", exc) logger.warning("deepsearch orchestration failed, using heuristic: %s", exc)

View File

@ -165,7 +165,7 @@ async def _run(payload: dict, output_dir: Path) -> dict:
should_stop = _make_stop(output_dir) should_stop = _make_stop(output_dir)
_stage("planning", "Planning research queries", PHASE_PLANNING) _stage("planning", "Planning research queries", PHASE_PLANNING)
queries = await plan_queries(query, api_key, _emit) queries, planner_cost = await plan_queries(query, api_key, _emit)
_emit({"type": "queries", "queries": queries}) _emit({"type": "queries", "queries": queries})
_stage("searching", "Searching the web", PHASE_SEARCHING) _stage("searching", "Searching the web", PHASE_SEARCHING)
@ -203,7 +203,8 @@ async def _run(payload: dict, output_dir: Path) -> dict:
_stage("analysis", "Running research agents", PHASE_ANALYSIS) _stage("analysis", "Running research agents", PHASE_ANALYSIS)
result = await orchestrate( result = await orchestrate(
query, outcome.pages, api_key, _emit, store=store, queries=queries query, outcome.pages, api_key, _emit, store=store, queries=queries,
planner_cost_usd=planner_cost,
) )
_stage("synthesis", "Compiling cited report", PHASE_SYNTHESIS) _stage("synthesis", "Compiling cited report", PHASE_SYNTHESIS)
@ -228,6 +229,7 @@ async def _run(payload: dict, output_dir: Path) -> dict:
"chunk_count": chunk_count, "chunk_count": chunk_count,
"embed_backend": embed_backend, "embed_backend": embed_backend,
"collection": collection, "collection": collection,
"cost_usd": result.total_cost_usd,
} }
(output_dir / "report.json").write_text( (output_dir / "report.json").write_text(
json.dumps(report, ensure_ascii=False), encoding="utf-8" json.dumps(report, ensure_ascii=False), encoding="utf-8"
@ -244,6 +246,7 @@ async def _run(payload: dict, output_dir: Path) -> dict:
"synthesis": report["synthesis"], "synthesis": report["synthesis"],
"page_count": report["page_count"], "page_count": report["page_count"],
"chunk_count": report["chunk_count"], "chunk_count": report["chunk_count"],
"cost_usd": report["cost_usd"],
} }
) )
return report return report

View File

@ -16,6 +16,9 @@
<span class="ds-metric"><strong>{{ source_diversity }}</strong> diversity</span> <span class="ds-metric"><strong>{{ source_diversity }}</strong> diversity</span>
<span class="ds-metric"><strong>{{ page_count }}</strong> sources</span> <span class="ds-metric"><strong>{{ page_count }}</strong> sources</span>
<span class="ds-metric"><strong>{{ chunk_count }}</strong> chunks</span> <span class="ds-metric"><strong>{{ chunk_count }}</strong> chunks</span>
{% if viewer_is_admin and cost_usd is not none %}
<span class="ds-metric"><strong>${{ "%.6f"|format(cost_usd) }}</strong> cost</span>
{% endif %}
</div> </div>
{% if export_md_url %} {% if export_md_url %}
<div class="ds-exports"> <div class="ds-exports">

8
dpc.log Normal file
View File

@ -0,0 +1,8 @@
2026-07-19T17:31:36 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T17:31:36 DEBUG model=molodetz-pro fps=30
2026-07-19T17:31:36 INFO read task from file: /workspace/prompts/research-10.txt
2026-07-19T17:31:36 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T19:05:24 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T19:05:24 DEBUG model=molodetz-pro fps=30
2026-07-19T19:05:24 INFO read task from file: /workspace/prompts/execution-1.txt
2026-07-19T19:05:24 INFO settings merged: model=<default> allow=0 deny=0 ask=0

View File

@ -39,6 +39,7 @@ def _seed_done_session(owner_id="ds-session-owner"):
"source_diversity": 0.5, "source_diversity": 0.5,
"page_count": 3, "page_count": 3,
"chunk_count": 12, "chunk_count": 12,
"cost_usd": 0.123456,
} }
result = { result = {
"query": "the question", "query": "the question",
@ -47,6 +48,7 @@ def _seed_done_session(owner_id="ds-session-owner"):
"source_diversity": 0.5, "source_diversity": 0.5,
"page_count": 3, "page_count": 3,
"chunk_count": 12, "chunk_count": 12,
"cost_usd": 0.123456,
"report": report, "report": report,
} }
get_table("jobs").update( get_table("jobs").update(
@ -97,6 +99,7 @@ def test_session_json_shape(app_server):
assert body["sources"] assert body["sources"]
assert "viewer_is_admin" in body assert "viewer_is_admin" in body
assert "viewer_owns" in body assert "viewer_owns" in body
assert body.get("cost_usd") is None # guest user, not admin
finally: finally:
_clear() _clear()
@ -108,6 +111,7 @@ def test_session_html_renders_without_jinja_global_collision(app_server):
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
assert "the question" in r.text assert "the question" in r.text
assert "dp-deepsearch-chat" in r.text assert "dp-deepsearch-chat" in r.text
assert "0.123456" not in r.text # guest user, cost hidden
finally: finally:
_clear() _clear()
@ -142,6 +146,7 @@ def test_session_reads_disk_report_before_result_commit(app_server):
"synthesis": "agents", "synthesis": "agents",
"page_count": 12, "page_count": 12,
"chunk_count": 61, "chunk_count": 61,
"cost_usd": 0.123456,
} }
session_dir = DEEPSEARCH_DIR / uid session_dir = DEEPSEARCH_DIR / uid
session_dir.mkdir(parents=True, exist_ok=True) session_dir.mkdir(parents=True, exist_ok=True)
@ -203,3 +208,26 @@ def test_export_json(app_server):
def test_export_unknown_uid_404(app_server): def test_export_unknown_uid_404(app_server):
r = requests.get(f"{BASE_URL}/tools/deepsearch/nope/export.md") r = requests.get(f"{BASE_URL}/tools/deepsearch/nope/export.md")
assert r.status_code == 404 assert r.status_code == 404
def test_session_admin_sees_cost(app_server, seeded_db):
try:
uid = _seed_done_session("alice_test")
s = requests.Session()
creds = seeded_db["alice"]
s.post(
f"{BASE_URL}/auth/login",
data={"email": creds["email"], "password": creds["password"]},
allow_redirects=True,
)
r = s.get(f"{BASE_URL}/tools/deepsearch/{uid}/session", headers=_json_headers())
assert r.status_code == 200, r.text
body = r.json()
assert body.get("cost_usd") == 0.123456
html = s.get(f"{BASE_URL}/tools/deepsearch/{uid}/session")
assert html.status_code == 200, html.text
assert "0.123456" in html.text
assert "cost" in html.text
finally:
_clear()