migrate knowledge atlas to llama.cpp

This commit is contained in:
wuyang
2026-07-28 17:16:34 +08:00
parent 88fb212985
commit efbd82532f
9 changed files with 152 additions and 109 deletions
+8 -7
View File
@@ -78,23 +78,24 @@ The existing evaluation contract must be revised against that research before ru
- Keep generated cache and PID/log files under `web/cache/`; they are intentionally ignored. - Keep generated cache and PID/log files under `web/cache/`; they are intentionally ignored.
- Never commit credentials. Git authentication is configured outside the repository. - Never commit credentials. Git authentication is configured outside the repository.
## Ollama Contract ## Model API Contract
Default endpoint: Default internal llama.cpp endpoints:
```text ```text
http://192.168.1.10:11434 Luna: http://192.168.1.10:11435/v1
Terra/Sol: http://192.168.1.10:11436/v1
``` ```
Current model tiers are configurable with: Current model tiers are configurable with:
```text ```text
OLLAMA_MODEL_LIGHT=ChatGPT-5.6:Luna MODEL_LIGHT=ChatGPT-5.6:Luna
OLLAMA_MODEL_FAST=ChatGPT-5.6:Terra MODEL_FAST=ChatGPT-5.6:Terra
OLLAMA_MODEL_LARGE=ChatGPT-5.6:Sol MODEL_LARGE=ChatGPT-5.6:Sol
``` ```
The application serializes Ollama calls because all models share one V100 32G GPU. Application requests must not send `num_ctx` or `keep_alive`; the Ollama server owns context size and residency. Expensive `Sol` actions should remain explicit, not automatic. Luna runs on the NAS AMD iGPU. Terra and Sol are aliases of one resident llama.cpp runner on the V100 32G. The application serializes model calls and must not override server context sizing. Expensive `Sol` actions should remain explicit, not automatic.
## Common Commands ## Common Commands
+7 -7
View File
@@ -85,7 +85,7 @@ evaluation contract.
| research tools | `tools/research/` | paginated search, abstract fetch, screening, classification, landscape build | | research tools | `tools/research/` | paginated search, abstract fetch, screening, classification, landscape build |
| experiments | `experiments/knowledge-compilation/` | failed pilot and completed full-text research audits | | experiments | `experiments/knowledge-compilation/` | failed pilot and completed full-text research audits |
| learning prototype | `learning/agent-memory/` | preserved failed 12-paper learning module | | learning prototype | `learning/agent-memory/` | preserved failed 12-paper learning module |
| web application | `web/` | Atlas, search agent, paper reader/chat, Ollama actions | | web application | `web/` | Atlas, search agent, paper reader/chat, llama.cpp actions |
| project integrity | `tools/check_project.py` | offline structural and data checks | | project integrity | `tools/check_project.py` | offline structural and data checks |
`projects/` and `references/` are still mostly scaffolding. They do not yet contain substantive project or general-reference catalogs. `projects/` and `references/` are still mostly scaffolding. They do not yet contain substantive project or general-reference catalogs.
@@ -161,7 +161,7 @@ Capabilities currently implemented:
- asynchronous search activity, aggregate statistics and AI brief; - asynchronous search activity, aggregate statistics and AI brief;
- follow-up questions on a search result; - follow-up questions on a search result;
- per-paper abstract fetch, summary, translation, deep analysis and chat; - per-paper abstract fetch, summary, translation, deep analysis and chat;
- single-concurrency Ollama access and local caches. - single-concurrency llama.cpp access and local caches.
Run it with: Run it with:
@@ -182,9 +182,9 @@ At the beginning of the 2026-07-12 handoff audit, no process was listening on 18
local and public health then returned 200. Runtime status is external state and must always be checked local and public health then returned 200. Runtime status is external state and must always be checked
live. After a corpus update, restart the service because the index is loaded at process startup. live. After a corpus update, restart the service because the index is loaded at process startup.
On 2026-07-27 both the Tailscale endpoint and `https://lab.k1412.top/api/health` returned the refreshed On 2026-07-27 both the Tailscale endpoint and `https://lab.k1412.top/api/health` returned the refreshed
1,125-paper count with `ollama_ready: true`. 1,125-paper count. After the 2026-07-28 migration, readiness is reported as `model_api_ready`.
The live Ollama inventory checked on 2026-07-12 included `ChatGPT-5.6:Luna`, `Terra`, `Sol`, `auto`, `embed`, and `gpt-4o:latest`. The web defaults were updated from stale `light/fast/large` tags to configurable Luna/Terra/Sol tiers. On 2026-07-28, inference moved from Ollama to two resident llama.cpp servers. Luna runs as Qwen3 0.6B Q8 on the NAS Radeon 780M through Vulkan. Terra and Sol share one Qwen3.6 27B Q4_K_M runner on the V100 through CUDA 12, preserving three public tiers without loading a second large model. The web application calls the two internal OpenAI-compatible endpoints directly.
The static Agent completion research document is packaged separately: The static Agent completion research document is packaged separately:
@@ -216,9 +216,9 @@ verified. The existing `AGENT EVAL` homepage entry remains under `projects` at o
## Runtime Constraints ## Runtime Constraints
- Python standard library only; no package installation is required for the current web server and collection scripts. - Python standard library only; no package installation is required for the current web server and collection scripts.
- Ollama endpoint defaults to `http://192.168.1.10:11434`. - llama.cpp endpoints default to `http://192.168.1.10:11435/v1` for Luna and `http://192.168.1.10:11436/v1` for Terra/Sol.
- One V100 32G serves all local models; AI calls are serialized. - Luna uses the NAS AMD iGPU; one V100 32G serves the shared Terra/Sol runner. AI calls are serialized.
- Do not send `num_ctx` or `keep_alive` from clients. Server-side context and residency must remain authoritative. - Context is fixed server-side at 8K for Luna and 32K for Terra/Sol.
- `Sol` is the expensive tier and should require an explicit user action. - `Sol` is the expensive tier and should require an explicit user action.
- The user mentioned an unlimited company GPT-5.5 data-agent API as a possible stronger offline worker, but no endpoint/authentication contract is committed. It is not currently integrated. - The user mentioned an unlimited company GPT-5.5 data-agent API as a possible stronger offline worker, but no endpoint/authentication contract is committed. It is not currently integrated.
- The public site has no repository-managed authentication or TLS configuration; the domain mapping is external infrastructure. - The public site has no repository-managed authentication or TLS configuration; the domain mapping is external infrastructure.
+10
View File
@@ -98,6 +98,16 @@ Decision retained: always verify `/api/tags`; never infer deployed model aliases
Relevant commits: `47c301c`, `ac6e0e0`, and the 2026-07-12 handoff update. Relevant commits: `47c301c`, `ac6e0e0`, and the 2026-07-12 handoff update.
On 2026-07-28 the runtime moved to llama.cpp after repeated Ollama runner churn and
resource-residency ambiguity. Luna was isolated on the NAS AMD iGPU through Vulkan,
while Terra and Sol became two API behaviors over one fixed 27B CUDA runner on the
V100. The web app now uses the servers' OpenAI-compatible endpoints, fixes context
at deployment time, and controls thinking by tier.
Decision retained: product tiers do not require separate physical model copies.
Keep resource ownership in the serving layer and let Terra/Sol share one resident
runner when their weights are identical.
## 6. Learning-System Reframing ## 6. Learning-System Reframing
The user clarified the real objective with three levels: The user clarified the real objective with three levels:
+5 -4
View File
@@ -113,10 +113,11 @@ def check_python_syntax() -> int:
return len(paths) return len(paths)
def check_ollama_contract() -> None: def check_model_api_contract() -> None:
source = (ROOT / "web" / "app.py").read_text(encoding="utf-8") source = (ROOT / "web" / "app.py").read_text(encoding="utf-8")
require('"num_ctx"' not in source, "web app must not override Ollama num_ctx") require('"num_ctx"' not in source, "web app must not override model API context")
require('"keep_alive"' not in source, "web app must not override Ollama keep_alive") require('"keep_alive"' not in source, "web app must not send legacy keep_alive")
require("/api/generate" not in source, "web app must use the OpenAI-compatible model API")
def check_experiment_state() -> None: def check_experiment_state() -> None:
@@ -207,7 +208,7 @@ def main() -> int:
collections = check_collection_index() collections = check_collection_index()
research = check_research_data() research = check_research_data()
python_files = check_python_syntax() python_files = check_python_syntax()
check_ollama_contract() check_model_api_contract()
check_experiment_state() check_experiment_state()
evaluation_tasks = check_evaluation_contract() evaluation_tasks = check_evaluation_contract()
completion_evidence = check_evaluation_site() completion_evidence = check_evaluation_site()
+8 -7
View File
@@ -15,10 +15,11 @@ web/manage.sh status
python3 web/app.py --host 100.114.68.27 --port 18080 python3 web/app.py --host 100.114.68.27 --port 18080
``` ```
默认 Ollama 地址: 默认 llama.cpp 地址:
```bash ```bash
OLLAMA_URL=http://192.168.1.10:11434 MODEL_API_LUNA_URL=http://192.168.1.10:11435/v1
MODEL_API_SOL_URL=http://192.168.1.10:11436/v1
``` ```
访问地址: 访问地址:
@@ -44,16 +45,16 @@ https://lab.k1412.top/
- 主题导读、阅读路径、主题比较:`ChatGPT-5.6:Sol`,只在前端手动确认后调用 - 主题导读、阅读路径、主题比较:`ChatGPT-5.6:Sol`,只在前端手动确认后调用
- Search Agent 查询理解:`ChatGPT-5.6:Luna` - Search Agent 查询理解:`ChatGPT-5.6:Luna`
- Search Agent 结果简报、搜索追问、论文对话:`ChatGPT-5.6:Terra` - Search Agent 结果简报、搜索追问、论文对话:`ChatGPT-5.6:Terra`
- Ollama 请求单并发执行,并缓存结果到 `web/cache/ai/` - llama.cpp 请求单并发执行,并缓存结果到 `web/cache/ai/`
- 应用侧不下发 `num_ctx` / `keep_alive`,上下文长度和模型常驻由 Ollama 服务端统一控制 - 应用侧不覆盖上下文长度,Luna 固定 8KTerra/Sol 固定 32K
- arXiv 摘要缓存到 `web/cache/arxiv/` - arXiv 摘要缓存到 `web/cache/arxiv/`
模型 tier 可以覆盖: 模型 tier 可以覆盖:
```bash ```bash
OLLAMA_MODEL_LIGHT=ChatGPT-5.6:Luna \ MODEL_LIGHT=ChatGPT-5.6:Luna \
OLLAMA_MODEL_FAST=ChatGPT-5.6:Terra \ MODEL_FAST=ChatGPT-5.6:Terra \
OLLAMA_MODEL_LARGE=ChatGPT-5.6:Sol \ MODEL_LARGE=ChatGPT-5.6:Sol \
web/manage.sh restart web/manage.sh restart
``` ```
+5 -4
View File
@@ -7,10 +7,11 @@ Wants=network-online.target
Type=simple Type=simple
WorkingDirectory=%h/Code/agent WorkingDirectory=%h/Code/agent
Environment=PYTHONUNBUFFERED=1 Environment=PYTHONUNBUFFERED=1
Environment=OLLAMA_URL=http://192.168.1.10:11434 Environment=MODEL_API_LUNA_URL=http://192.168.1.10:11435/v1
Environment=OLLAMA_MODEL_LIGHT=ChatGPT-5.6:Luna Environment=MODEL_API_SOL_URL=http://192.168.1.10:11436/v1
Environment=OLLAMA_MODEL_FAST=ChatGPT-5.6:Terra Environment=MODEL_LIGHT=ChatGPT-5.6:Luna
Environment=OLLAMA_MODEL_LARGE=ChatGPT-5.6:Sol Environment=MODEL_FAST=ChatGPT-5.6:Terra
Environment=MODEL_LARGE=ChatGPT-5.6:Sol
Environment=HOST=100.114.68.27 Environment=HOST=100.114.68.27
Environment=PORT=18080 Environment=PORT=18080
EnvironmentFile=-%h/Code/agent/web/cache/service.env EnvironmentFile=-%h/Code/agent/web/cache/service.env
+98 -70
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Serve a local paper browser with lightweight Ollama-backed actions.""" """Serve a local paper browser with lightweight llama.cpp-backed actions."""
from __future__ import annotations from __future__ import annotations
@@ -30,8 +30,13 @@ INDEX_PATH = ROOT / "data" / "index.json"
CACHE_ROOT = WEB_ROOT / "cache" CACHE_ROOT = WEB_ROOT / "cache"
ARXIV_CACHE = CACHE_ROOT / "arxiv" ARXIV_CACHE = CACHE_ROOT / "arxiv"
AI_CACHE = CACHE_ROOT / "ai" AI_CACHE = CACHE_ROOT / "ai"
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://192.168.1.10:11434").rstrip("/") MODEL_API_LUNA_URL = os.environ.get(
ASSET_VERSION = "atlas-20260712-01" "MODEL_API_LUNA_URL", "http://192.168.1.10:11435/v1"
).rstrip("/")
MODEL_API_SOL_URL = os.environ.get(
"MODEL_API_SOL_URL", "http://192.168.1.10:11436/v1"
).rstrip("/")
ASSET_VERSION = "atlas-20260728-01"
def detect_app_version() -> str: def detect_app_version() -> str:
@@ -51,9 +56,9 @@ def detect_app_version() -> str:
APP_VERSION = detect_app_version() APP_VERSION = detect_app_version()
MODEL_LIGHT = os.environ.get("OLLAMA_MODEL_LIGHT", "ChatGPT-5.6:Luna") MODEL_LIGHT = os.environ.get("MODEL_LIGHT", "ChatGPT-5.6:Luna")
MODEL_FAST = os.environ.get("OLLAMA_MODEL_FAST", "ChatGPT-5.6:Terra") MODEL_FAST = os.environ.get("MODEL_FAST", "ChatGPT-5.6:Terra")
MODEL_LARGE = os.environ.get("OLLAMA_MODEL_LARGE", "ChatGPT-5.6:Sol") MODEL_LARGE = os.environ.get("MODEL_LARGE", "ChatGPT-5.6:Sol")
DEFAULT_MODELS = { DEFAULT_MODELS = {
"translate": MODEL_LIGHT, "translate": MODEL_LIGHT,
@@ -75,7 +80,7 @@ NS = {
} }
INDEX_LOCK = threading.Lock() INDEX_LOCK = threading.Lock()
OLLAMA_LOCK = threading.Lock() MODEL_API_LOCK = threading.Lock()
SEARCH_LOCK = threading.Lock() SEARCH_LOCK = threading.Lock()
INDEX_CACHE: dict[str, Any] = {"mtime": 0.0, "papers": [], "by_id": {}} INDEX_CACHE: dict[str, Any] = {"mtime": 0.0, "papers": [], "by_id": {}}
ATLAS_CACHE: dict[str, Any] = {"mtime": 0.0, "atlas": None} ATLAS_CACHE: dict[str, Any] = {"mtime": 0.0, "atlas": None}
@@ -512,8 +517,8 @@ def plan_search_query(query: str, scope_topic: str = "") -> tuple[dict[str, Any]
model = DEFAULT_MODELS["search_plan"] model = DEFAULT_MODELS["search_plan"]
result: dict[str, Any] | None = None result: dict[str, Any] | None = None
try: try:
with OLLAMA_LOCK: with MODEL_API_LOCK:
result = call_ollama(model, search_plan_prompt(query, scope_topic), "search_plan") result = call_model(model, search_plan_prompt(query, scope_topic), "search_plan")
plan = extract_json_object(result["response"]) plan = extract_json_object(result["response"])
merged = { merged = {
**fallback, **fallback,
@@ -676,8 +681,8 @@ def fallback_search_summary(query: str, plan: dict[str, Any], aggregation: dict[
def generate_search_summary(query: str, plan: dict[str, Any], items: list[dict[str, Any]], aggregation: dict[str, Any]) -> dict[str, Any]: def generate_search_summary(query: str, plan: dict[str, Any], items: list[dict[str, Any]], aggregation: dict[str, Any]) -> dict[str, Any]:
model = DEFAULT_MODELS["search_summary"] model = DEFAULT_MODELS["search_summary"]
try: try:
with OLLAMA_LOCK: with MODEL_API_LOCK:
result = call_ollama(model, search_summary_prompt(query, plan, items, aggregation), "search_summary") result = call_model(model, search_summary_prompt(query, plan, items, aggregation), "search_summary")
return { return {
"text": result["response"], "text": result["response"],
"model": result["model"], "model": result["model"],
@@ -1240,45 +1245,84 @@ def clean_model_response(value: str) -> str:
return text or value.strip() return text or value.strip()
def call_ollama(model: str, prompt: str, mode: str) -> dict[str, Any]: def model_api_url(model: str) -> str:
if model in {MODEL_LIGHT, "ChatGPT-5.6:auto", "gpt-4o"}:
return MODEL_API_LUNA_URL
return MODEL_API_SOL_URL
def model_api_request(url: str, *, data: bytes | None = None) -> urllib.request.Request:
headers = {"content-type": "application/json"} if data is not None else {}
return urllib.request.Request(url, data=data, headers=headers)
def available_model_inventory(timeout: float = 5) -> list[dict[str, Any]]:
aliases: set[str] = set()
for base_url in (MODEL_API_LUNA_URL, MODEL_API_SOL_URL):
request = model_api_request(f"{base_url}/models")
with urllib.request.urlopen(request, timeout=timeout) as response:
payload = json.loads(response.read().decode("utf-8"))
for item in payload.get("data", []):
if item.get("id"):
aliases.add(str(item["id"]))
aliases.update(str(alias) for alias in item.get("aliases", []) if alias)
context_lengths = {
MODEL_LIGHT: 8192,
MODEL_FAST: 32768,
MODEL_LARGE: 32768,
}
return [
{
"name": model,
"parameter_size": None,
"context_length": context_lengths.get(model),
}
for model in sorted(aliases)
]
def call_model(model: str, prompt: str, mode: str) -> dict[str, Any]:
options = { options = {
"temperature": 0.2, "temperature": 0.2,
"num_predict": 520, "max_tokens": 520,
} }
if mode == "translate": if mode == "translate":
options.update({"num_predict": 700, "temperature": 0.1}) options.update({"max_tokens": 700, "temperature": 0.1})
elif mode == "deep": elif mode == "deep":
options.update({"num_predict": 900, "temperature": 0.25}) options.update({"max_tokens": 900, "temperature": 0.25})
elif mode in {"atlas", "topic", "path", "compare"}: elif mode in {"atlas", "topic", "path", "compare"}:
options.update({"num_predict": 950, "temperature": 0.22}) options.update({"max_tokens": 950, "temperature": 0.22})
elif mode == "search_plan": elif mode == "search_plan":
options.update({"num_predict": 420, "temperature": 0.1}) options.update({"max_tokens": 420, "temperature": 0.1})
elif mode in {"search_summary", "search_followup", "paper_chat"}: elif mode in {"search_summary", "search_followup", "paper_chat"}:
options.update({"num_predict": 780, "temperature": 0.18}) options.update({"max_tokens": 780, "temperature": 0.18})
payload = { payload = {
"model": model, "model": model,
"prompt": prompt, "messages": [{"role": "user", "content": prompt}],
"stream": False, "stream": False,
"options": options, "chat_template_kwargs": {"enable_thinking": model == MODEL_LARGE},
**options,
} }
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
request = urllib.request.Request( request = model_api_request(
f"{OLLAMA_URL}/api/generate", f"{model_api_url(model)}/chat/completions",
data=data, data=data,
headers={"content-type": "application/json"},
method="POST",
) )
started = time.time() started = time.time()
with urllib.request.urlopen(request, timeout=240) as response: with urllib.request.urlopen(request, timeout=240) as response:
result = json.loads(response.read().decode("utf-8")) result = json.loads(response.read().decode("utf-8"))
choice = (result.get("choices") or [{}])[0]
message = choice.get("message") or {}
usage = result.get("usage") or {}
return { return {
"model": model, "model": model,
"mode": mode, "mode": mode,
"response": clean_model_response(str(result.get("response") or "")), "response": clean_model_response(str(message.get("content") or "")),
"duration_seconds": round(time.time() - started, 2), "duration_seconds": round(time.time() - started, 2),
"eval_count": result.get("eval_count"), "eval_count": usage.get("completion_tokens"),
"prompt_eval_count": result.get("prompt_eval_count"), "prompt_eval_count": usage.get("prompt_tokens"),
} }
@@ -1379,20 +1423,14 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
def handle_health(self) -> None: def handle_health(self) -> None:
papers, _ = load_papers() papers, _ = load_papers()
ollama_ok = False model_api_ok = False
available_models: list[str] = [] available_models: list[str] = []
try: try:
request = urllib.request.Request(f"{OLLAMA_URL}/api/tags") inventory = available_model_inventory(timeout=3)
with urllib.request.urlopen(request, timeout=3) as response: available_models = [str(item["name"]) for item in inventory]
ollama_ok = response.status == 200 model_api_ok = True
payload = json.loads(response.read().decode("utf-8"))
available_models = sorted(
str(item.get("name"))
for item in payload.get("models", [])
if item.get("name")
)
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError): except (urllib.error.URLError, TimeoutError, json.JSONDecodeError):
ollama_ok = False model_api_ok = False
missing_default_models = sorted(set(DEFAULT_MODELS.values()) - set(available_models)) missing_default_models = sorted(set(DEFAULT_MODELS.values()) - set(available_models))
write_json( write_json(
self, self,
@@ -1401,9 +1439,9 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
"paper_count": len(papers), "paper_count": len(papers),
"app_version": APP_VERSION, "app_version": APP_VERSION,
"asset_version": ASSET_VERSION, "asset_version": ASSET_VERSION,
"ollama_url": OLLAMA_URL, "model_api_urls": [MODEL_API_LUNA_URL, MODEL_API_SOL_URL],
"ollama_ok": ollama_ok, "model_api_ok": model_api_ok,
"ollama_ready": ollama_ok and not missing_default_models, "model_api_ready": model_api_ok and not missing_default_models,
"available_models": available_models, "available_models": available_models,
"missing_default_models": missing_default_models, "missing_default_models": missing_default_models,
"default_models": DEFAULT_MODELS, "default_models": DEFAULT_MODELS,
@@ -1412,20 +1450,10 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
def handle_models(self) -> None: def handle_models(self) -> None:
try: try:
request = urllib.request.Request(f"{OLLAMA_URL}/api/tags") models = available_model_inventory()
with urllib.request.urlopen(request, timeout=5) as response:
payload = json.loads(response.read().decode("utf-8"))
models = [
{
"name": item.get("name"),
"parameter_size": (item.get("details") or {}).get("parameter_size"),
"context_length": (item.get("details") or {}).get("context_length"),
}
for item in payload.get("models", [])
]
write_json(self, {"models": models, "default_models": DEFAULT_MODELS}) write_json(self, {"models": models, "default_models": DEFAULT_MODELS})
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
write_error(self, HTTPStatus.BAD_GATEWAY, f"ollama unavailable: {exc}") write_error(self, HTTPStatus.BAD_GATEWAY, f"model API unavailable: {exc}")
def handle_paper_search(self, query: dict[str, list[str]]) -> None: def handle_paper_search(self, query: dict[str, list[str]]) -> None:
papers, _ = load_papers() papers, _ = load_papers()
@@ -1508,11 +1536,11 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
if not job or job.get("status") != "done": if not job or job.get("status") != "done":
write_error(self, HTTPStatus.NOT_FOUND, "completed search job not found") write_error(self, HTTPStatus.NOT_FOUND, "completed search job not found")
return return
if not OLLAMA_LOCK.acquire(blocking=False): if not MODEL_API_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "ollama is busy; try again later") write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "model API is busy; try again later")
return return
try: try:
result = call_ollama(model, search_followup_prompt(job, question), "search_followup") result = call_model(model, search_followup_prompt(job, question), "search_followup")
write_json( write_json(
self, self,
{ {
@@ -1524,7 +1552,7 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
}, },
) )
finally: finally:
OLLAMA_LOCK.release() MODEL_API_LOCK.release()
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc)) write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
@@ -1581,8 +1609,8 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
if not paper: if not paper:
write_error(self, HTTPStatus.NOT_FOUND, "paper not found") write_error(self, HTTPStatus.NOT_FOUND, "paper not found")
return return
if not OLLAMA_LOCK.acquire(blocking=False): if not MODEL_API_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "ollama is busy; try again later") write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "model API is busy; try again later")
return return
try: try:
abstract = None abstract = None
@@ -1592,7 +1620,7 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
abstract = fetch_arxiv(arxiv_id) abstract = fetch_arxiv(arxiv_id)
except Exception: except Exception:
abstract = None abstract = None
result = call_ollama(model, paper_chat_prompt(paper, abstract, question, history), "paper_chat") result = call_model(model, paper_chat_prompt(paper, abstract, question, history), "paper_chat")
write_json( write_json(
self, self,
{ {
@@ -1604,7 +1632,7 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
}, },
) )
finally: finally:
OLLAMA_LOCK.release() MODEL_API_LOCK.release()
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc)) write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
@@ -1633,8 +1661,8 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
write_json(self, result) write_json(self, result)
return return
if not OLLAMA_LOCK.acquire(blocking=False): if not MODEL_API_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "ollama is busy; try again later") write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "model API is busy; try again later")
return return
try: try:
abstract = None abstract = None
@@ -1642,7 +1670,7 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
if arxiv_id: if arxiv_id:
abstract = fetch_arxiv(arxiv_id) abstract = fetch_arxiv(arxiv_id)
context = paper_context(paper, abstract) context = paper_context(paper, abstract)
result = call_ollama(model, prompt_for(mode, context), mode) result = call_model(model, prompt_for(mode, context), mode)
result.update( result.update(
{ {
"id": paper_id, "id": paper_id,
@@ -1654,7 +1682,7 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
cache_path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") cache_path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
write_json(self, result) write_json(self, result)
finally: finally:
OLLAMA_LOCK.release() MODEL_API_LOCK.release()
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc)) write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
@@ -1685,12 +1713,12 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
write_json(self, result) write_json(self, result)
return return
if not OLLAMA_LOCK.acquire(blocking=False): if not MODEL_API_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "ollama is busy; try again later") write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "model API is busy; try again later")
return return
try: try:
context = atlas_context(topic, topic_b) context = atlas_context(topic, topic_b)
result = call_ollama(model, atlas_prompt_for(mode, context), mode) result = call_model(model, atlas_prompt_for(mode, context), mode)
result.update( result.update(
{ {
"scope": cache_key, "scope": cache_key,
@@ -1701,7 +1729,7 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
cache_path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") cache_path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
write_json(self, result) write_json(self, result)
finally: finally:
OLLAMA_LOCK.release() MODEL_API_LOCK.release()
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc)) write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
@@ -1719,7 +1747,7 @@ def main() -> int:
load_papers() load_papers()
server = ThreadingHTTPServer((args.host, args.port), PaperBrowserHandler) server = ThreadingHTTPServer((args.host, args.port), PaperBrowserHandler)
print(f"Paper browser: http://{args.host}:{args.port}") print(f"Paper browser: http://{args.host}:{args.port}")
print(f"Ollama: {OLLAMA_URL}") print(f"Model APIs: Luna={MODEL_API_LUNA_URL}, Sol/Terra={MODEL_API_SOL_URL}")
try: try:
server.serve_forever() server.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
+5 -4
View File
@@ -15,10 +15,11 @@ write_environment() {
{ {
printf 'HOST=%s\n' "$HOST" printf 'HOST=%s\n' "$HOST"
printf 'PORT=%s\n' "$PORT" printf 'PORT=%s\n' "$PORT"
printf 'OLLAMA_URL=%s\n' "${OLLAMA_URL:-http://192.168.1.10:11434}" printf 'MODEL_API_LUNA_URL=%s\n' "${MODEL_API_LUNA_URL:-http://192.168.1.10:11435/v1}"
printf 'OLLAMA_MODEL_LIGHT=%s\n' "${OLLAMA_MODEL_LIGHT:-ChatGPT-5.6:Luna}" printf 'MODEL_API_SOL_URL=%s\n' "${MODEL_API_SOL_URL:-http://192.168.1.10:11436/v1}"
printf 'OLLAMA_MODEL_FAST=%s\n' "${OLLAMA_MODEL_FAST:-ChatGPT-5.6:Terra}" printf 'MODEL_LIGHT=%s\n' "${MODEL_LIGHT:-ChatGPT-5.6:Luna}"
printf 'OLLAMA_MODEL_LARGE=%s\n' "${OLLAMA_MODEL_LARGE:-ChatGPT-5.6:Sol}" printf 'MODEL_FAST=%s\n' "${MODEL_FAST:-ChatGPT-5.6:Terra}"
printf 'MODEL_LARGE=%s\n' "${MODEL_LARGE:-ChatGPT-5.6:Sol}"
} >"$ENV_FILE" } >"$ENV_FILE"
} }
+6 -6
View File
@@ -141,12 +141,12 @@ async function loadHealth() {
try { try {
const health = await api("/api/health"); const health = await api("/api/health");
state.health = health; state.health = health;
const ollamaState = health.ollama_ready const modelState = health.model_api_ready
? "Ollama ready" ? "Model API ready"
: health.ollama_ok : health.model_api_ok
? "Ollama model mismatch" ? "Model API mismatch"
: "Ollama offline"; : "Model API offline";
elements.healthLine.textContent = `${health.paper_count} papers · ${ollamaState}`; elements.healthLine.textContent = `${health.paper_count} papers · ${modelState}`;
} catch (error) { } catch (error) {
elements.healthLine.textContent = error.message; elements.healthLine.textContent = error.message;
} }