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)
viewer_is_admin = is_admin(user)
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,
"status": queue.DONE if done else job.get("status", ""),
"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_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,
"cost_usd": cost_usd if viewer_is_admin else None,
"viewer_is_admin": viewer_is_admin,
"viewer_owns": _owns(request, session),
"created_at": session.get("created_at"),
@ -255,6 +257,7 @@ def _session_context(request: Request, uid: str, job: dict, session: dict) -> di
"user": user,
"meta_robots": "noindex,nofollow",
}
return ctx
@router.get("/{uid}/session")
async def deepsearch_session(request: Request, uid: str):

View File

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

View File

@ -7,6 +7,7 @@ import time
from devplacepy import stealth
from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL
from devplacepy.services.openai_gateway.usage import parse_usage_headers
logger = logging.getLogger(__name__)
@ -41,7 +42,10 @@ async def request_completion(
if response.status_code >= 400:
raise RuntimeError(f"chat gateway returned {response.status_code}")
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(

View File

@ -5,10 +5,11 @@ from __future__ import annotations
import json
import logging
import re
from typing import Callable
from typing import Callable, Tuple
from devplacepy import stealth
from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL
from devplacepy.services.openai_gateway.usage import parse_usage_headers
from .phases import PHASE_PLANNING
@ -61,7 +62,7 @@ def _noop(frame: dict) -> None:
async def plan_queries(
query: str, api_key: str, emit: Callable[[dict], None] = _noop
) -> list[str]:
) -> Tuple[list[str], float]:
emit(
{
"type": "substep",
@ -89,6 +90,8 @@ async def plan_queries(
)
if response.status_code >= 400:
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()
content = (
(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",
}
)
return parsed
return parsed, planner_cost
except Exception as exc:
logger.warning("deepsearch query planner failed, using fallback: %s", exc)
fallback = _fallback(query)
@ -113,4 +116,4 @@ async def plan_queries(
"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
score: int = 0
synthesis: str = "agents"
total_cost_usd: float = 0.0
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_out": int(raw_usage.get("completion_tokens") or len(text) // 4),
"elapsed_ms": elapsed_ms,
"cost_usd": float(raw_usage.get("cost_usd", 0.0)),
}
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),
"tokens_in": usage.get("tokens_in", 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],
store=None,
queries: list[str] | None = None,
planner_cost_usd: float = 0.0,
) -> Orchestration:
diversity = source_diversity(pages)
if not pages:
@ -418,6 +422,12 @@ async def orchestrate(
score = int(
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(
summary=summary,
findings=findings,
@ -425,6 +435,7 @@ async def orchestrate(
source_diversity=diversity,
score=score,
synthesis="agents",
total_cost_usd=total_cost,
)
except Exception as 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)
_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})
_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)
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)
@ -228,6 +229,7 @@ async def _run(payload: dict, output_dir: Path) -> dict:
"chunk_count": chunk_count,
"embed_backend": embed_backend,
"collection": collection,
"cost_usd": result.total_cost_usd,
}
(output_dir / "report.json").write_text(
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"],
"page_count": report["page_count"],
"chunk_count": report["chunk_count"],
"cost_usd": report["cost_usd"],
}
)
return report

View File

@ -16,6 +16,9 @@
<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>{{ 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>
{% if export_md_url %}
<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,
"page_count": 3,
"chunk_count": 12,
"cost_usd": 0.123456,
}
result = {
"query": "the question",
@ -47,6 +48,7 @@ def _seed_done_session(owner_id="ds-session-owner"):
"source_diversity": 0.5,
"page_count": 3,
"chunk_count": 12,
"cost_usd": 0.123456,
"report": report,
}
get_table("jobs").update(
@ -97,6 +99,7 @@ def test_session_json_shape(app_server):
assert body["sources"]
assert "viewer_is_admin" in body
assert "viewer_owns" in body
assert body.get("cost_usd") is None # guest user, not admin
finally:
_clear()
@ -108,6 +111,7 @@ def test_session_html_renders_without_jinja_global_collision(app_server):
assert r.status_code == 200, r.text
assert "the question" in r.text
assert "dp-deepsearch-chat" in r.text
assert "0.123456" not in r.text # guest user, cost hidden
finally:
_clear()
@ -142,6 +146,7 @@ def test_session_reads_disk_report_before_result_commit(app_server):
"synthesis": "agents",
"page_count": 12,
"chunk_count": 61,
"cost_usd": 0.123456,
}
session_dir = DEEPSEARCH_DIR / uid
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):
r = requests.get(f"{BASE_URL}/tools/deepsearch/nope/export.md")
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()