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
@@ -15,10 +15,11 @@ web/manage.sh status
python3 web/app.py --host 100.114.68.27 --port 18080
```
默认 Ollama 地址:
默认 llama.cpp 地址:
```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`,只在前端手动确认后调用
- Search Agent 查询理解:`ChatGPT-5.6:Luna`
- Search Agent 结果简报、搜索追问、论文对话:`ChatGPT-5.6:Terra`
- Ollama 请求单并发执行,并缓存结果到 `web/cache/ai/`
- 应用侧不下发 `num_ctx` / `keep_alive`,上下文长度和模型常驻由 Ollama 服务端统一控制
- llama.cpp 请求单并发执行,并缓存结果到 `web/cache/ai/`
- 应用侧不覆盖上下文长度,Luna 固定 8KTerra/Sol 固定 32K
- arXiv 摘要缓存到 `web/cache/arxiv/`
模型 tier 可以覆盖:
```bash
OLLAMA_MODEL_LIGHT=ChatGPT-5.6:Luna \
OLLAMA_MODEL_FAST=ChatGPT-5.6:Terra \
OLLAMA_MODEL_LARGE=ChatGPT-5.6:Sol \
MODEL_LIGHT=ChatGPT-5.6:Luna \
MODEL_FAST=ChatGPT-5.6:Terra \
MODEL_LARGE=ChatGPT-5.6:Sol \
web/manage.sh restart
```
+5 -4
View File
@@ -7,10 +7,11 @@ Wants=network-online.target
Type=simple
WorkingDirectory=%h/Code/agent
Environment=PYTHONUNBUFFERED=1
Environment=OLLAMA_URL=http://192.168.1.10:11434
Environment=OLLAMA_MODEL_LIGHT=ChatGPT-5.6:Luna
Environment=OLLAMA_MODEL_FAST=ChatGPT-5.6:Terra
Environment=OLLAMA_MODEL_LARGE=ChatGPT-5.6:Sol
Environment=MODEL_API_LUNA_URL=http://192.168.1.10:11435/v1
Environment=MODEL_API_SOL_URL=http://192.168.1.10:11436/v1
Environment=MODEL_LIGHT=ChatGPT-5.6:Luna
Environment=MODEL_FAST=ChatGPT-5.6:Terra
Environment=MODEL_LARGE=ChatGPT-5.6:Sol
Environment=HOST=100.114.68.27
Environment=PORT=18080
EnvironmentFile=-%h/Code/agent/web/cache/service.env
+98 -70
View File
@@ -1,5 +1,5 @@
#!/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
@@ -30,8 +30,13 @@ INDEX_PATH = ROOT / "data" / "index.json"
CACHE_ROOT = WEB_ROOT / "cache"
ARXIV_CACHE = CACHE_ROOT / "arxiv"
AI_CACHE = CACHE_ROOT / "ai"
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://192.168.1.10:11434").rstrip("/")
ASSET_VERSION = "atlas-20260712-01"
MODEL_API_LUNA_URL = os.environ.get(
"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:
@@ -51,9 +56,9 @@ def detect_app_version() -> str:
APP_VERSION = detect_app_version()
MODEL_LIGHT = os.environ.get("OLLAMA_MODEL_LIGHT", "ChatGPT-5.6:Luna")
MODEL_FAST = os.environ.get("OLLAMA_MODEL_FAST", "ChatGPT-5.6:Terra")
MODEL_LARGE = os.environ.get("OLLAMA_MODEL_LARGE", "ChatGPT-5.6:Sol")
MODEL_LIGHT = os.environ.get("MODEL_LIGHT", "ChatGPT-5.6:Luna")
MODEL_FAST = os.environ.get("MODEL_FAST", "ChatGPT-5.6:Terra")
MODEL_LARGE = os.environ.get("MODEL_LARGE", "ChatGPT-5.6:Sol")
DEFAULT_MODELS = {
"translate": MODEL_LIGHT,
@@ -75,7 +80,7 @@ NS = {
}
INDEX_LOCK = threading.Lock()
OLLAMA_LOCK = threading.Lock()
MODEL_API_LOCK = threading.Lock()
SEARCH_LOCK = threading.Lock()
INDEX_CACHE: dict[str, Any] = {"mtime": 0.0, "papers": [], "by_id": {}}
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"]
result: dict[str, Any] | None = None
try:
with OLLAMA_LOCK:
result = call_ollama(model, search_plan_prompt(query, scope_topic), "search_plan")
with MODEL_API_LOCK:
result = call_model(model, search_plan_prompt(query, scope_topic), "search_plan")
plan = extract_json_object(result["response"])
merged = {
**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]:
model = DEFAULT_MODELS["search_summary"]
try:
with OLLAMA_LOCK:
result = call_ollama(model, search_summary_prompt(query, plan, items, aggregation), "search_summary")
with MODEL_API_LOCK:
result = call_model(model, search_summary_prompt(query, plan, items, aggregation), "search_summary")
return {
"text": result["response"],
"model": result["model"],
@@ -1240,45 +1245,84 @@ def clean_model_response(value: str) -> str:
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 = {
"temperature": 0.2,
"num_predict": 520,
"max_tokens": 520,
}
if mode == "translate":
options.update({"num_predict": 700, "temperature": 0.1})
options.update({"max_tokens": 700, "temperature": 0.1})
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"}:
options.update({"num_predict": 950, "temperature": 0.22})
options.update({"max_tokens": 950, "temperature": 0.22})
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"}:
options.update({"num_predict": 780, "temperature": 0.18})
options.update({"max_tokens": 780, "temperature": 0.18})
payload = {
"model": model,
"prompt": prompt,
"messages": [{"role": "user", "content": prompt}],
"stream": False,
"options": options,
"chat_template_kwargs": {"enable_thinking": model == MODEL_LARGE},
**options,
}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
request = urllib.request.Request(
f"{OLLAMA_URL}/api/generate",
request = model_api_request(
f"{model_api_url(model)}/chat/completions",
data=data,
headers={"content-type": "application/json"},
method="POST",
)
started = time.time()
with urllib.request.urlopen(request, timeout=240) as response:
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 {
"model": model,
"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),
"eval_count": result.get("eval_count"),
"prompt_eval_count": result.get("prompt_eval_count"),
"eval_count": usage.get("completion_tokens"),
"prompt_eval_count": usage.get("prompt_tokens"),
}
@@ -1379,20 +1423,14 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
def handle_health(self) -> None:
papers, _ = load_papers()
ollama_ok = False
model_api_ok = False
available_models: list[str] = []
try:
request = urllib.request.Request(f"{OLLAMA_URL}/api/tags")
with urllib.request.urlopen(request, timeout=3) as response:
ollama_ok = response.status == 200
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")
)
inventory = available_model_inventory(timeout=3)
available_models = [str(item["name"]) for item in inventory]
model_api_ok = True
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))
write_json(
self,
@@ -1401,9 +1439,9 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
"paper_count": len(papers),
"app_version": APP_VERSION,
"asset_version": ASSET_VERSION,
"ollama_url": OLLAMA_URL,
"ollama_ok": ollama_ok,
"ollama_ready": ollama_ok and not missing_default_models,
"model_api_urls": [MODEL_API_LUNA_URL, MODEL_API_SOL_URL],
"model_api_ok": model_api_ok,
"model_api_ready": model_api_ok and not missing_default_models,
"available_models": available_models,
"missing_default_models": missing_default_models,
"default_models": DEFAULT_MODELS,
@@ -1412,20 +1450,10 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
def handle_models(self) -> None:
try:
request = urllib.request.Request(f"{OLLAMA_URL}/api/tags")
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", [])
]
models = available_model_inventory()
write_json(self, {"models": models, "default_models": DEFAULT_MODELS})
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:
papers, _ = load_papers()
@@ -1508,11 +1536,11 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
if not job or job.get("status") != "done":
write_error(self, HTTPStatus.NOT_FOUND, "completed search job not found")
return
if not OLLAMA_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "ollama is busy; try again later")
if not MODEL_API_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "model API is busy; try again later")
return
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(
self,
{
@@ -1524,7 +1552,7 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
},
)
finally:
OLLAMA_LOCK.release()
MODEL_API_LOCK.release()
except Exception as exc: # noqa: BLE001
write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
@@ -1581,8 +1609,8 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
if not paper:
write_error(self, HTTPStatus.NOT_FOUND, "paper not found")
return
if not OLLAMA_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "ollama is busy; try again later")
if not MODEL_API_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "model API is busy; try again later")
return
try:
abstract = None
@@ -1592,7 +1620,7 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
abstract = fetch_arxiv(arxiv_id)
except Exception:
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(
self,
{
@@ -1604,7 +1632,7 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
},
)
finally:
OLLAMA_LOCK.release()
MODEL_API_LOCK.release()
except Exception as exc: # noqa: BLE001
write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
@@ -1633,8 +1661,8 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
write_json(self, result)
return
if not OLLAMA_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "ollama is busy; try again later")
if not MODEL_API_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "model API is busy; try again later")
return
try:
abstract = None
@@ -1642,7 +1670,7 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
if arxiv_id:
abstract = fetch_arxiv(arxiv_id)
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(
{
"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")
write_json(self, result)
finally:
OLLAMA_LOCK.release()
MODEL_API_LOCK.release()
except Exception as exc: # noqa: BLE001
write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
@@ -1685,12 +1713,12 @@ class PaperBrowserHandler(SimpleHTTPRequestHandler):
write_json(self, result)
return
if not OLLAMA_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "ollama is busy; try again later")
if not MODEL_API_LOCK.acquire(blocking=False):
write_error(self, HTTPStatus.TOO_MANY_REQUESTS, "model API is busy; try again later")
return
try:
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(
{
"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")
write_json(self, result)
finally:
OLLAMA_LOCK.release()
MODEL_API_LOCK.release()
except Exception as exc: # noqa: BLE001
write_error(self, HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
@@ -1719,7 +1747,7 @@ def main() -> int:
load_papers()
server = ThreadingHTTPServer((args.host, args.port), PaperBrowserHandler)
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:
server.serve_forever()
except KeyboardInterrupt:
+5 -4
View File
@@ -15,10 +15,11 @@ write_environment() {
{
printf 'HOST=%s\n' "$HOST"
printf 'PORT=%s\n' "$PORT"
printf 'OLLAMA_URL=%s\n' "${OLLAMA_URL:-http://192.168.1.10:11434}"
printf 'OLLAMA_MODEL_LIGHT=%s\n' "${OLLAMA_MODEL_LIGHT:-ChatGPT-5.6:Luna}"
printf 'OLLAMA_MODEL_FAST=%s\n' "${OLLAMA_MODEL_FAST:-ChatGPT-5.6:Terra}"
printf 'OLLAMA_MODEL_LARGE=%s\n' "${OLLAMA_MODEL_LARGE:-ChatGPT-5.6:Sol}"
printf 'MODEL_API_LUNA_URL=%s\n' "${MODEL_API_LUNA_URL:-http://192.168.1.10:11435/v1}"
printf 'MODEL_API_SOL_URL=%s\n' "${MODEL_API_SOL_URL:-http://192.168.1.10:11436/v1}"
printf 'MODEL_LIGHT=%s\n' "${MODEL_LIGHT:-ChatGPT-5.6:Luna}"
printf 'MODEL_FAST=%s\n' "${MODEL_FAST:-ChatGPT-5.6:Terra}"
printf 'MODEL_LARGE=%s\n' "${MODEL_LARGE:-ChatGPT-5.6:Sol}"
} >"$ENV_FILE"
}
+6 -6
View File
@@ -141,12 +141,12 @@ async function loadHealth() {
try {
const health = await api("/api/health");
state.health = health;
const ollamaState = health.ollama_ready
? "Ollama ready"
: health.ollama_ok
? "Ollama model mismatch"
: "Ollama offline";
elements.healthLine.textContent = `${health.paper_count} papers · ${ollamaState}`;
const modelState = health.model_api_ready
? "Model API ready"
: health.model_api_ok
? "Model API mismatch"
: "Model API offline";
elements.healthLine.textContent = `${health.paper_count} papers · ${modelState}`;
} catch (error) {
elements.healthLine.textContent = error.message;
}