migrate knowledge atlas to llama.cpp
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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 固定 8K,Terra/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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user