-
Notifications
You must be signed in to change notification settings - Fork 12
refactor: dashboard/settings redesign + keychain API key storage #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5f16ad6
af9cb79
10bc536
13d65e7
e386516
c2dfae4
e71153b
ea6ad1f
5a7419d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||
| from __future__ import annotations | ||||
|
|
||||
| import os | ||||
| import time | ||||
| from typing import Any, Dict, List, Optional | ||||
|
|
||||
| from fastapi import APIRouter, HTTPException | ||||
|
|
@@ -66,10 +67,13 @@ class EndpointTestRequest(BaseModel): | |||
|
|
||||
| class EndpointTestResponse(BaseModel): | ||||
| ok: bool | ||||
| success: bool | ||||
| endpoint_id: int | ||||
| provider: Dict[str, Any] | ||||
| api_key_present: bool | ||||
| latency_ms: int | ||||
| message: str | ||||
| error: Optional[str] = None | ||||
|
|
||||
|
|
||||
| class EndpointCapabilitiesResponse(BaseModel): | ||||
|
|
@@ -169,6 +173,7 @@ def test_model_endpoint(endpoint_id: int, req: EndpointTestRequest): | |||
| os.getenv(api_key_env) | ||||
| ) | ||||
|
|
||||
| t0 = time.monotonic() | ||||
| try: | ||||
| cfg = _build_model_config(endpoint) | ||||
| router = ModelRouter(RouterConfig(models={"__test__": cfg}, fallback_model="__test__")) | ||||
|
|
@@ -187,16 +192,21 @@ def test_model_endpoint(endpoint_id: int, req: EndpointTestRequest): | |||
| ) | ||||
| message = f"Remote check success: {(pong or '').strip()[:80]}" | ||||
|
|
||||
| latency_ms = int((time.monotonic() - t0) * 1000) | ||||
| return EndpointTestResponse( | ||||
| ok=True, | ||||
| success=True, | ||||
| endpoint_id=endpoint_id, | ||||
| provider={ | ||||
| "provider_name": info.provider_name, | ||||
| "model_name": info.model_name, | ||||
| "api_base": info.api_base, | ||||
| }, | ||||
| api_key_present=api_key_present, | ||||
| latency_ms=latency_ms, | ||||
| message=message, | ||||
| error=None, | ||||
| ) | ||||
| except Exception as exc: | ||||
| latency_ms = int((time.monotonic() - t0) * 1000) | ||||
|
||||
| latency_ms = int((time.monotonic() - t0) * 1000) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any, Dict, List, Optional, Tuple | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from fastapi import APIRouter, BackgroundTasks, HTTPException, Query | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from fastapi.responses import Response | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import BaseModel, Field | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sqlalchemy.exc import IntegrityError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1119,6 +1120,13 @@ class WorkflowMetricsResponse(BaseModel): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| summary: Dict[str, Any] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class EvidenceCoverageResponse(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| coverage_rate: float | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_claims: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_with_evidence: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trend: List[Dict[str, Any]] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @router.get("/research/metrics/workflows", response_model=WorkflowMetricsResponse) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_workflow_metrics_summary( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| days: int = Query(7, ge=1, le=90), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1133,6 +1141,37 @@ def get_workflow_metrics_summary( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return WorkflowMetricsResponse(summary=summary) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @router.get("/research/metrics/evidence-coverage", response_model=EvidenceCoverageResponse) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_evidence_coverage( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| days: int = Query(7, ge=1, le=90), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workflow: Optional[str] = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| track_id: Optional[int] = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| summary = _get_workflow_metric_store().summarize( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| days=days, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workflow=workflow, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| track_id=track_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| totals = summary.get("totals", {}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_claims = int(totals.get("claim_count") or 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_with_evidence = int(totals.get("evidence_count") or 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| coverage_rate = float(totals.get("coverage_rate") or 0.0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trend = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for day_bucket in summary.get("by_day", []): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trend.append({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "date": day_bucket.get("date", ""), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "rate": float(day_bucket.get("coverage_rate") or 0.0), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return EvidenceCoverageResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| coverage_rate=coverage_rate, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_claims=total_claims, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_with_evidence=total_with_evidence, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trend=trend, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @router.post("/research/context", response_model=ContextResponse) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def build_context(req: ContextRequest): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set_trace_id() # Initialize trace_id for this request | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1608,3 +1647,139 @@ async def scholar_trends(req: ScholarTrendsRequest): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recent_papers=recent_papers[:10], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trend_summary=trend_summary, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Paper export (BibTeX / RIS / Markdown) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _make_citation_key(authors: List[str], year: Optional[int]) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """first_author_lastname + year, e.g. 'smith2025'.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastname = "unknown" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if authors: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts = authors[0].strip().split() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if parts: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastname = re.sub(r"[^a-zA-Z]", "", parts[-1]).lower() or "unknown" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return f"{lastname}{year or 'nd'}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _dedup_citation_keys(keys: List[str]) -> List[str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Append a/b/c suffixes when keys collide.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seen: Dict[str, int] = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result: List[str] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for k in keys: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| count = seen.get(k, 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seen[k] = count + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result.append(k if count == 0 else f"{k}{chr(ord('a') + count)}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result.append(k if count == 0 else f"{k}{chr(ord('a') + count)}") | |
| result.append(k if count == 0 else f"{k}{chr(ord('a') + count - 1)}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
当前用于通过附加字符后缀(chr(ord('a') + count))来去重引用键的实现,在处理超过25个相同基本键的冲突时存在问题。如果 count 达到26或更高,它将开始附加非字母字符(例如 {, |),这可能导致无效的BibTeX键。虽然这是一个边缘情况,但更稳健的做法是处理它。例如,在'z'之后切换到数字后缀,或者使用类似Excel列的 a, b, ..., z, aa, ab, ... 风格的后缀。以下建议实现了后者,它能优雅地处理任意数量的冲突。此建议还将后缀序列更改为从 'a' 开始,这更为常规。
| def _dedup_citation_keys(keys: List[str]) -> List[str]: | |
| """Append a/b/c suffixes when keys collide.""" | |
| seen: Dict[str, int] = {} | |
| result: List[str] = [] | |
| for k in keys: | |
| count = seen.get(k, 0) | |
| seen[k] = count + 1 | |
| result.append(k if count == 0 else f"{k}{chr(ord('a') + count)}") | |
| return result | |
| def _dedup_citation_keys(keys: List[str]) -> List[str]: | |
| """Append a/b/c suffixes when keys collide.""" | |
| seen: Dict[str, int] = {} | |
| result: List[str] = [] | |
| for k in keys: | |
| count = seen.get(k, 0) | |
| seen[k] = count + 1 | |
| if count == 0: | |
| result.append(k) | |
| continue | |
| # Generate suffixes like a, b, ..., z, aa, ab, ... | |
| suffix = "" | |
| temp_count = count | |
| while temp_count > 0: | |
| temp_count, remainder = divmod(temp_count - 1, 26) | |
| suffix = chr(ord('a') + remainder) + suffix | |
| result.append(f"{k}{suffix}") | |
| return result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EndpointTestResponse模型同时包含了ok: bool和success: bool两个字段。这两个字段似乎是多余的,因为在成功的情况下它们都被设置为True。为了简化数据模型并提高清晰度,我建议移除ok字段。您也需要在test_model_endpoint函数中移除对它的使用。