G-ACE commited on
Commit
775cbe6
·
verified ·
1 Parent(s): aaa4456

Deploy free local 4B+4B+8B 829d4932ef224f7b02587a37d9948c47459042ac KB kb-20260814T042741+0900

Browse files
Files changed (41) hide show
  1. Dockerfile +64 -3
  2. config/model.yaml +25 -11
  3. deployment-manifest.json +8 -2
  4. pyproject.toml +1 -1
  5. requirements.txt +1 -0
  6. runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  7. runtime/__pycache__/answer_quality.cpython-312.pyc +0 -0
  8. runtime/__pycache__/bootstrap.cpython-312.pyc +0 -0
  9. runtime/__pycache__/contracts.cpython-312.pyc +0 -0
  10. runtime/__pycache__/hf_client.cpython-312.pyc +0 -0
  11. runtime/__pycache__/integration.cpython-312.pyc +0 -0
  12. runtime/__pycache__/internal_core.cpython-312.pyc +0 -0
  13. runtime/__pycache__/japanese_skills.cpython-312.pyc +0 -0
  14. runtime/__pycache__/kagrra_bridge.cpython-312.pyc +0 -0
  15. runtime/__pycache__/kb_bucket.cpython-312.pyc +0 -0
  16. runtime/__pycache__/kb_harvest.cpython-312.pyc +0 -0
  17. runtime/__pycache__/kb_search.cpython-312.pyc +0 -0
  18. runtime/__pycache__/knowledge.cpython-312.pyc +0 -0
  19. runtime/__pycache__/live_state.cpython-312.pyc +0 -0
  20. runtime/__pycache__/model.cpython-312.pyc +0 -0
  21. runtime/__pycache__/observability.cpython-312.pyc +0 -0
  22. runtime/__pycache__/quality.cpython-312.pyc +0 -0
  23. runtime/__pycache__/roles.cpython-312.pyc +0 -0
  24. runtime/__pycache__/runtime_factory.cpython-312.pyc +0 -0
  25. runtime/__pycache__/schemas.cpython-312.pyc +0 -0
  26. runtime/__pycache__/search_planner.cpython-312.pyc +0 -0
  27. runtime/__pycache__/security.cpython-312.pyc +0 -0
  28. runtime/__pycache__/service.cpython-312.pyc +0 -0
  29. runtime/__pycache__/shared_head.cpython-312.pyc +0 -0
  30. runtime/__pycache__/skill_runtime.cpython-312.pyc +0 -0
  31. runtime/__pycache__/startup.cpython-312.pyc +0 -0
  32. runtime/__pycache__/state.cpython-312.pyc +0 -0
  33. runtime/__pycache__/task_decomposition.cpython-312.pyc +0 -0
  34. runtime/__pycache__/v8_bridge.cpython-312.pyc +0 -0
  35. runtime/__pycache__/writing_skills.cpython-312.pyc +0 -0
  36. runtime/bootstrap.py +15 -5
  37. runtime/hf_client.py +36 -15
  38. runtime/model.py +68 -11
  39. runtime/runtime_factory.py +19 -12
  40. runtime/startup.py +15 -9
  41. scripts/start_local_cpu.sh +76 -0
Dockerfile CHANGED
@@ -1,19 +1,80 @@
1
- FROM python:3.13-slim
 
 
2
 
3
  ENV PYTHONDONTWRITEBYTECODE=1 \
4
  PYTHONUNBUFFERED=1 \
5
- PORT=7860
 
 
 
 
 
 
6
 
7
  WORKDIR /app
8
 
 
 
 
 
 
 
9
  COPY requirements.txt /app/requirements.txt
10
  RUN python -m pip install --no-cache-dir --upgrade pip \
11
  && python -m pip install --no-cache-dir -r /app/requirements.txt
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  COPY app.py /app/app.py
14
  COPY runtime /app/runtime
15
  COPY config /app/config
 
 
16
 
17
  EXPOSE 7860
18
 
19
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ FROM ghcr.io/ggml-org/llama.cpp:server AS llama_cpp
2
+
3
+ FROM python:3.12-slim
4
 
5
  ENV PYTHONDONTWRITEBYTECODE=1 \
6
  PYTHONUNBUFFERED=1 \
7
+ PORT=7860 \
8
+ LD_LIBRARY_PATH=/opt/llama \
9
+ CUSTOMER_AI_CONSTRUCTIVE_API_URL=http://127.0.0.1:8081/v1/chat/completions \
10
+ CUSTOMER_AI_ADVERSARIAL_API_URL=http://127.0.0.1:8082/v1/chat/completions \
11
+ CUSTOMER_AI_EVIDENCE_API_URL=http://127.0.0.1:8083/v1/chat/completions \
12
+ CUSTOMER_AI_LOCAL_4B_PATH=/models/llm-jp-3-3.7b-instruct3-Q4_K_M.gguf \
13
+ CUSTOMER_AI_LOCAL_8B_PATH=/models/llm-jp-4-8b-instruct-Q4_K_M.gguf
14
 
15
  WORKDIR /app
16
 
17
+ RUN apt-get update \
18
+ && apt-get install -y --no-install-recommends ca-certificates curl libgomp1 \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ COPY --from=llama_cpp /app /opt/llama
22
+
23
  COPY requirements.txt /app/requirements.txt
24
  RUN python -m pip install --no-cache-dir --upgrade pip \
25
  && python -m pip install --no-cache-dir -r /app/requirements.txt
26
 
27
+ # Download immutable, public GGUF weights at build time. These are local model
28
+ # files, not Hugging Face Inference Provider calls, so runtime inference credits
29
+ # are never consumed.
30
+ RUN mkdir -p /models \
31
+ && python - <<'PY'
32
+ from hashlib import sha256
33
+ from pathlib import Path
34
+ from huggingface_hub import hf_hub_download
35
+
36
+ targets = [
37
+ {
38
+ "repo": "mmnga/llm-jp-3-3.7b-instruct3-gguf",
39
+ "revision": "7edef5a4f094ec8c1aed1e196c6a544675efbc2f",
40
+ "filename": "llm-jp-3-3.7b-instruct3-Q4_K_M.gguf",
41
+ "sha256": "a4a09d2141717a01b44e7a8dbdb28da8c01e9078c8051367cd6a20f7008ef5a8",
42
+ },
43
+ {
44
+ "repo": "mmnga-o/llm-jp-4-8b-instruct-gguf",
45
+ "revision": "7ae4da12cee2f109509cb8e1d01cf8a0f1a5fbc1",
46
+ "filename": "llm-jp-4-8b-instruct-Q4_K_M.gguf",
47
+ "sha256": "b6a61b9c8d4e7cb1ae543d8fcf472c9fb9abfc5d48af17f5017ce89c2dc0bd56",
48
+ },
49
+ ]
50
+
51
+ for item in targets:
52
+ path = Path(
53
+ hf_hub_download(
54
+ repo_id=item["repo"],
55
+ revision=item["revision"],
56
+ filename=item["filename"],
57
+ local_dir="/models",
58
+ )
59
+ )
60
+ h = sha256()
61
+ with path.open("rb") as handle:
62
+ for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""):
63
+ h.update(chunk)
64
+ actual = h.hexdigest()
65
+ if actual != item["sha256"]:
66
+ raise SystemExit(
67
+ f"local_model_sha_mismatch file={item['filename']} expected={item['sha256']} actual={actual}"
68
+ )
69
+ print(f"LOCAL_MODEL_PINNED={path.name} SHA256={actual}")
70
+ PY
71
+
72
  COPY app.py /app/app.py
73
  COPY runtime /app/runtime
74
  COPY config /app/config
75
+ COPY scripts/start_local_cpu.sh /app/scripts/start_local_cpu.sh
76
+ RUN chmod +x /app/scripts/start_local_cpu.sh
77
 
78
  EXPOSE 7860
79
 
80
+ CMD ["/app/scripts/start_local_cpu.sh"]
config/model.yaml CHANGED
@@ -1,19 +1,33 @@
1
  model:
2
  topology: one_work_three_role_models
3
  constructive:
4
- model_id: Qwen/Qwen3-4B
5
- route_alias: qwen3:4b
6
- revision: null
 
 
 
 
7
  adversarial:
8
- model_id: Qwen/Qwen3-4B
9
- route_alias: qwen3:4b
10
- revision: null
 
 
 
 
11
  evidence_bound:
12
- model_id: Qwen/Qwen3-8B
13
- route_alias: qwen3:8b
14
- revision: null
15
- backend_mode: benchmark_required
16
- role_adapter_mode: benchmark_required
 
 
 
 
 
 
17
  require_trained_domain_model: true
18
  allow_untrained_production_model: false
19
  zero_gpu: false
 
1
  model:
2
  topology: one_work_three_role_models
3
  constructive:
4
+ model_id: llm-jp/llm-jp-3-3.7b-instruct3
5
+ class: 4B
6
+ backend: local_llama_cpp
7
+ endpoint: http://127.0.0.1:8081/v1/chat/completions
8
+ gguf_repo: mmnga/llm-jp-3-3.7b-instruct3-gguf
9
+ revision: 7edef5a4f094ec8c1aed1e196c6a544675efbc2f
10
+ gguf_sha256: a4a09d2141717a01b44e7a8dbdb28da8c01e9078c8051367cd6a20f7008ef5a8
11
  adversarial:
12
+ model_id: llm-jp/llm-jp-3-3.7b-instruct3
13
+ class: 4B
14
+ backend: local_llama_cpp
15
+ endpoint: http://127.0.0.1:8082/v1/chat/completions
16
+ gguf_repo: mmnga/llm-jp-3-3.7b-instruct3-gguf
17
+ revision: 7edef5a4f094ec8c1aed1e196c6a544675efbc2f
18
+ gguf_sha256: a4a09d2141717a01b44e7a8dbdb28da8c01e9078c8051367cd6a20f7008ef5a8
19
  evidence_bound:
20
+ model_id: llm-jp/llm-jp-4-8b-instruct
21
+ class: 8B
22
+ backend: local_llama_cpp
23
+ endpoint: http://127.0.0.1:8083/v1/chat/completions
24
+ gguf_repo: mmnga-o/llm-jp-4-8b-instruct-gguf
25
+ revision: 7ae4da12cee2f109509cb8e1d01cf8a0f1a5fbc1
26
+ gguf_sha256: b6a61b9c8d4e7cb1ae543d8fcf472c9fb9abfc5d48af17f5017ce89c2dc0bd56
27
+ backend_mode: local_cpu_basic_no_inference_provider
28
+ role_adapter_mode: independent_role_processes
29
+ inference_provider_allowed: false
30
+ inference_provider_credit_burn_allowed: false
31
  require_trained_domain_model: true
32
  allow_untrained_production_model: false
33
  zero_gpu: false
deployment-manifest.json CHANGED
@@ -1,7 +1,13 @@
1
  {
2
  "schema_version": 5,
3
- "kb_storage": "huggingface_private_bucket_direct_runtime",
 
 
4
  "kb_bucket_id": "G-ACE/astera-customerai-kb",
5
  "kb_build_id": "kb-20260814T042741+0900",
6
- "kb_embedded_in_space": false
 
 
 
 
7
  }
 
1
  {
2
  "schema_version": 5,
3
+ "source_sha": "829d4932ef224f7b02587a37d9948c47459042ac",
4
+ "generated_at": "2026-08-23T07:31:44.635267+00:00",
5
+ "kb_storage": "huggingface_private_bucket_remote_read",
6
  "kb_bucket_id": "G-ACE/astera-customerai-kb",
7
  "kb_build_id": "kb-20260814T042741+0900",
8
+ "kb_active_pointer": "active.json",
9
+ "kb_embedded_in_space": false,
10
+ "role_topology": "local_cpu_4b_4b_8b",
11
+ "inference_provider": "disabled",
12
+ "deployment_path": "direct_hf_hub_cpu_basic"
13
  }
pyproject.toml CHANGED
@@ -2,7 +2,7 @@
2
  name = "astera-customer-ai"
3
  version = "0.0.0"
4
  requires-python = ">=3.11"
5
- dependencies = ["fastapi>=0.128,<1", "uvicorn>=0.48,<1", "pydantic>=2.12,<3", "httpx>=0.28,<1", "rapidfuzz==3.14.3", "pyyaml>=6,<7", "huggingface_hub>=1.24,<2"]
6
 
7
  [project.optional-dependencies]
8
  training = ["datasets", "transformers", "peft", "trl"]
 
2
  name = "astera-customer-ai"
3
  version = "0.0.0"
4
  requires-python = ">=3.11"
5
+ dependencies = ["fastapi>=0.128,<1", "uvicorn>=0.48,<1", "pydantic>=2.12,<3", "httpx>=0.28,<1", "rapidfuzz==3.14.3", "pyyaml>=6,<7", "huggingface_hub>=1.24,<2", "hf-xet>=1.1,<2"]
6
 
7
  [project.optional-dependencies]
8
  training = ["datasets", "transformers", "peft", "trl"]
requirements.txt CHANGED
@@ -5,3 +5,4 @@ httpx>=0.28,<1
5
  rapidfuzz==3.14.3
6
  pyyaml>=6,<7
7
  huggingface_hub>=1.24,<2
 
 
5
  rapidfuzz==3.14.3
6
  pyyaml>=6,<7
7
  huggingface_hub>=1.24,<2
8
+ hf-xet>=1.1,<2
runtime/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (361 Bytes). View file
 
runtime/__pycache__/answer_quality.cpython-312.pyc ADDED
Binary file (6.72 kB). View file
 
runtime/__pycache__/bootstrap.cpython-312.pyc ADDED
Binary file (3.37 kB). View file
 
runtime/__pycache__/contracts.cpython-312.pyc ADDED
Binary file (4.79 kB). View file
 
runtime/__pycache__/hf_client.cpython-312.pyc ADDED
Binary file (4.38 kB). View file
 
runtime/__pycache__/integration.cpython-312.pyc ADDED
Binary file (4.1 kB). View file
 
runtime/__pycache__/internal_core.cpython-312.pyc ADDED
Binary file (20.1 kB). View file
 
runtime/__pycache__/japanese_skills.cpython-312.pyc ADDED
Binary file (12.1 kB). View file
 
runtime/__pycache__/kagrra_bridge.cpython-312.pyc ADDED
Binary file (2.05 kB). View file
 
runtime/__pycache__/kb_bucket.cpython-312.pyc ADDED
Binary file (10.6 kB). View file
 
runtime/__pycache__/kb_harvest.cpython-312.pyc ADDED
Binary file (2.44 kB). View file
 
runtime/__pycache__/kb_search.cpython-312.pyc ADDED
Binary file (23.6 kB). View file
 
runtime/__pycache__/knowledge.cpython-312.pyc ADDED
Binary file (6.9 kB). View file
 
runtime/__pycache__/live_state.cpython-312.pyc ADDED
Binary file (2.06 kB). View file
 
runtime/__pycache__/model.cpython-312.pyc ADDED
Binary file (7.09 kB). View file
 
runtime/__pycache__/observability.cpython-312.pyc ADDED
Binary file (2.77 kB). View file
 
runtime/__pycache__/quality.cpython-312.pyc ADDED
Binary file (2.79 kB). View file
 
runtime/__pycache__/roles.cpython-312.pyc ADDED
Binary file (1.21 kB). View file
 
runtime/__pycache__/runtime_factory.cpython-312.pyc ADDED
Binary file (4.53 kB). View file
 
runtime/__pycache__/schemas.cpython-312.pyc ADDED
Binary file (7.45 kB). View file
 
runtime/__pycache__/search_planner.cpython-312.pyc ADDED
Binary file (3.79 kB). View file
 
runtime/__pycache__/security.cpython-312.pyc ADDED
Binary file (3.34 kB). View file
 
runtime/__pycache__/service.cpython-312.pyc ADDED
Binary file (1.23 kB). View file
 
runtime/__pycache__/shared_head.cpython-312.pyc ADDED
Binary file (9.93 kB). View file
 
runtime/__pycache__/skill_runtime.cpython-312.pyc ADDED
Binary file (4.01 kB). View file
 
runtime/__pycache__/startup.cpython-312.pyc ADDED
Binary file (8.03 kB). View file
 
runtime/__pycache__/state.cpython-312.pyc ADDED
Binary file (17.8 kB). View file
 
runtime/__pycache__/task_decomposition.cpython-312.pyc ADDED
Binary file (9.84 kB). View file
 
runtime/__pycache__/v8_bridge.cpython-312.pyc ADDED
Binary file (2.59 kB). View file
 
runtime/__pycache__/writing_skills.cpython-312.pyc ADDED
Binary file (5.78 kB). View file
 
runtime/bootstrap.py CHANGED
@@ -3,7 +3,13 @@ from __future__ import annotations
3
  from dataclasses import dataclass
4
  from typing import Iterable, Mapping
5
 
6
- from .hf_client import HF_CHAT_API, HF_MODEL_4B, HF_MODEL_8B
 
 
 
 
 
 
7
  from .japanese_skills import JapaneseShortQASkillPack
8
  from .runtime_factory import InternalRuntimeDependencies, build_internal_core
9
  from .service import CustomerAIWork
@@ -19,13 +25,15 @@ class RuntimeDependencies:
19
  kb_generation_id: str | None = None
20
  hf_token: str | None = None
21
  role_pool: object | None = None
22
- shared_head: object | None = None # compatibility injection only; not a single-model contract
23
  max_targeted_retry: int = 1
24
  constructive_model_id: str = HF_MODEL_4B
25
  adversarial_model_id: str = HF_MODEL_4B
26
  evidence_model_id: str = HF_MODEL_8B
27
- hf_api_url: str = HF_CHAT_API
28
- timeout_seconds: float = 30.0
 
 
29
 
30
 
31
  def build_work(deps: RuntimeDependencies) -> CustomerAIWork:
@@ -51,7 +59,9 @@ def build_work(deps: RuntimeDependencies) -> CustomerAIWork:
51
  constructive_model_id=deps.constructive_model_id,
52
  adversarial_model_id=deps.adversarial_model_id,
53
  evidence_model_id=deps.evidence_model_id,
54
- hf_api_url=deps.hf_api_url,
 
 
55
  timeout_seconds=deps.timeout_seconds,
56
  )
57
  )
 
3
  from dataclasses import dataclass
4
  from typing import Iterable, Mapping
5
 
6
+ from .hf_client import (
7
+ HF_CHAT_API_ADVERSARIAL,
8
+ HF_CHAT_API_CONSTRUCTIVE,
9
+ HF_CHAT_API_EVIDENCE,
10
+ HF_MODEL_4B,
11
+ HF_MODEL_8B,
12
+ )
13
  from .japanese_skills import JapaneseShortQASkillPack
14
  from .runtime_factory import InternalRuntimeDependencies, build_internal_core
15
  from .service import CustomerAIWork
 
25
  kb_generation_id: str | None = None
26
  hf_token: str | None = None
27
  role_pool: object | None = None
28
+ shared_head: object | None = None
29
  max_targeted_retry: int = 1
30
  constructive_model_id: str = HF_MODEL_4B
31
  adversarial_model_id: str = HF_MODEL_4B
32
  evidence_model_id: str = HF_MODEL_8B
33
+ constructive_api_url: str = HF_CHAT_API_CONSTRUCTIVE
34
+ adversarial_api_url: str = HF_CHAT_API_ADVERSARIAL
35
+ evidence_api_url: str = HF_CHAT_API_EVIDENCE
36
+ timeout_seconds: float = 300.0
37
 
38
 
39
  def build_work(deps: RuntimeDependencies) -> CustomerAIWork:
 
59
  constructive_model_id=deps.constructive_model_id,
60
  adversarial_model_id=deps.adversarial_model_id,
61
  evidence_model_id=deps.evidence_model_id,
62
+ constructive_api_url=deps.constructive_api_url,
63
+ adversarial_api_url=deps.adversarial_api_url,
64
+ evidence_api_url=deps.evidence_api_url,
65
  timeout_seconds=deps.timeout_seconds,
66
  )
67
  )
runtime/hf_client.py CHANGED
@@ -5,31 +5,47 @@ from collections.abc import Sequence
5
 
6
  import httpx
7
 
8
- HF_MODEL_4B = "Qwen/Qwen3-4B"
9
- HF_MODEL_8B = "Qwen/Qwen3-8B"
10
  HF_ALLOWED_MODELS = frozenset({HF_MODEL_4B, HF_MODEL_8B})
11
- HF_CHAT_API = "https://router.huggingface.co/v1/chat/completions"
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
 
14
  class HFChatClient:
15
- """Role-scoped Hugging Face chat client. Multiple role clients may share one HTTP pool."""
 
 
 
 
16
 
17
  def __init__(
18
  self,
19
  *,
20
- token: str,
21
  model_id: str,
22
- api_url: str = HF_CHAT_API,
23
- timeout_seconds: float = 30.0,
24
  client: httpx.AsyncClient | None = None,
25
  ):
26
  if model_id not in HF_ALLOWED_MODELS:
27
  raise ValueError(f"model_drift:{model_id}")
28
- if not token.strip() and client is None:
29
- raise ValueError("hf_token_required")
 
30
  self.model_id = model_id
31
- self.api_url = api_url
32
- self._token = token.strip()
33
  self._owned = client is None
34
  self._client = client or httpx.AsyncClient(timeout=timeout_seconds)
35
 
@@ -37,7 +53,7 @@ class HFChatClient:
37
  self,
38
  messages: Sequence[dict[str, str]],
39
  *,
40
- max_tokens: int = 1800,
41
  ) -> dict[str, object]:
42
  payload = {
43
  "model": self.model_id,
@@ -45,10 +61,10 @@ class HFChatClient:
45
  "response_format": {"type": "json_object"},
46
  "stream": False,
47
  "temperature": 0.1,
 
48
  "max_tokens": max_tokens,
49
  }
50
- headers = {"authorization": f"Bearer {self._token}"} if self._token else {}
51
- response = await self._client.post(self.api_url, headers=headers, json=payload)
52
  response.raise_for_status()
53
  body = response.json()
54
  choices = body.get("choices") or []
@@ -57,7 +73,12 @@ class HFChatClient:
57
  content = choices[0].get("message", {}).get("content")
58
  if not isinstance(content, str) or not content.strip():
59
  raise RuntimeError("model_empty_content")
60
- decoded = json.loads(content)
 
 
 
 
 
61
  if not isinstance(decoded, dict):
62
  raise RuntimeError("model_non_object_json")
63
  return decoded
 
5
 
6
  import httpx
7
 
8
+ HF_MODEL_4B = "llm-jp/llm-jp-3-3.7b-instruct3"
9
+ HF_MODEL_8B = "llm-jp/llm-jp-4-8b-instruct"
10
  HF_ALLOWED_MODELS = frozenset({HF_MODEL_4B, HF_MODEL_8B})
11
+
12
+ HF_CHAT_API_CONSTRUCTIVE = "http://127.0.0.1:8081/v1/chat/completions"
13
+ HF_CHAT_API_ADVERSARIAL = "http://127.0.0.1:8082/v1/chat/completions"
14
+ HF_CHAT_API_EVIDENCE = "http://127.0.0.1:8083/v1/chat/completions"
15
+ HF_CHAT_API = HF_CHAT_API_CONSTRUCTIVE # compatibility alias only
16
+
17
+
18
+ def _default_local_url(model_id: str) -> str:
19
+ if model_id == HF_MODEL_4B:
20
+ return HF_CHAT_API_CONSTRUCTIVE
21
+ if model_id == HF_MODEL_8B:
22
+ return HF_CHAT_API_EVIDENCE
23
+ raise ValueError(f"model_drift:{model_id}")
24
 
25
 
26
  class HFChatClient:
27
+ """Role-scoped OpenAI-compatible client backed only by local llama.cpp.
28
+
29
+ Production rejects non-loopback endpoints so HF Inference Providers cannot
30
+ be used accidentally and cannot consume inference credits.
31
+ """
32
 
33
  def __init__(
34
  self,
35
  *,
36
+ token: str = "",
37
  model_id: str,
38
+ api_url: str | None = None,
39
+ timeout_seconds: float = 600.0,
40
  client: httpx.AsyncClient | None = None,
41
  ):
42
  if model_id not in HF_ALLOWED_MODELS:
43
  raise ValueError(f"model_drift:{model_id}")
44
+ resolved_url = (api_url or _default_local_url(model_id)).strip()
45
+ if not resolved_url.startswith(("http://127.0.0.1", "http://localhost")):
46
+ raise ValueError("remote_inference_endpoint_forbidden")
47
  self.model_id = model_id
48
+ self.api_url = resolved_url
 
49
  self._owned = client is None
50
  self._client = client or httpx.AsyncClient(timeout=timeout_seconds)
51
 
 
53
  self,
54
  messages: Sequence[dict[str, str]],
55
  *,
56
+ max_tokens: int = 900,
57
  ) -> dict[str, object]:
58
  payload = {
59
  "model": self.model_id,
 
61
  "response_format": {"type": "json_object"},
62
  "stream": False,
63
  "temperature": 0.1,
64
+ "top_p": 0.9,
65
  "max_tokens": max_tokens,
66
  }
67
+ response = await self._client.post(self.api_url, json=payload)
 
68
  response.raise_for_status()
69
  body = response.json()
70
  choices = body.get("choices") or []
 
73
  content = choices[0].get("message", {}).get("content")
74
  if not isinstance(content, str) or not content.strip():
75
  raise RuntimeError("model_empty_content")
76
+ text = content.strip()
77
+ first = text.find("{")
78
+ last = text.rfind("}")
79
+ if first >= 0 and last >= first:
80
+ text = text[first : last + 1]
81
+ decoded = json.loads(text)
82
  if not isinstance(decoded, dict):
83
  raise RuntimeError("model_non_object_json")
84
  return decoded
runtime/model.py CHANGED
@@ -7,6 +7,13 @@ from typing import Protocol
7
 
8
  import httpx
9
 
 
 
 
 
 
 
 
10
  from .roles import role_rules
11
  from .schemas import RoleName, RoleResult, SharedRolePacket
12
 
@@ -39,26 +46,76 @@ class ResidentRolePool:
39
  return await self._workers[role].run(packet)
40
 
41
 
 
 
 
 
 
 
 
42
  class HuggingFaceRoleBackend:
43
- def __init__(self, *, role: RoleName, model_id: str, token: str, api_url: str = "https://router.huggingface.co/v1/chat/completions", timeout_seconds: float = 30.0):
44
- if not model_id.strip(): raise ValueError("model_id_required")
45
- if not token.strip(): raise ValueError("hf_token_required")
46
- self.role, self.model_id, self.token, self.api_url, self.timeout_seconds = role, model_id.strip(), token.strip(), api_url, timeout_seconds
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  async def generate_role(self, role: RoleName, packet: SharedRolePacket) -> RoleResult:
49
- if role != self.role: raise ValueError("backend_role_mismatch")
 
50
  prompt = {
51
  "role": role.value,
52
  "rules": role_rules(role),
53
  "packet": packet.model_dump(mode="json"),
54
  "required_output_schema": RoleResult.model_json_schema(),
55
- "constraints": ["Use only supplied packet facts for Astera-specific claims.", "Return JSON only.", "Do not claim external actions were executed."],
 
 
 
 
56
  }
57
  async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
58
- response = await client.post(self.api_url, headers={"authorization": f"Bearer {self.token}"}, json={"model": self.model_id, "messages": [{"role": "user", "content": json.dumps(prompt, ensure_ascii=False)}], "response_format": {"type": "json_object"}, "stream": False})
 
 
 
 
 
 
 
 
59
  response.raise_for_status()
60
- payload = response.json(); choices = payload.get("choices") or []
61
- if not choices: raise RuntimeError("model_empty_choices")
 
 
62
  content = choices[0].get("message", {}).get("content")
63
- if not isinstance(content, str) or not content.strip(): raise RuntimeError("model_empty_content")
64
- return RoleResult.model_validate_json(content)
 
 
 
 
 
 
 
7
 
8
  import httpx
9
 
10
+ from .hf_client import (
11
+ HF_CHAT_API_ADVERSARIAL,
12
+ HF_CHAT_API_CONSTRUCTIVE,
13
+ HF_CHAT_API_EVIDENCE,
14
+ HF_MODEL_4B,
15
+ HF_MODEL_8B,
16
+ )
17
  from .roles import role_rules
18
  from .schemas import RoleName, RoleResult, SharedRolePacket
19
 
 
46
  return await self._workers[role].run(packet)
47
 
48
 
49
+ _LOCAL_ROLE_CONFIG = {
50
+ RoleName.CONSTRUCTIVE: (HF_MODEL_4B, HF_CHAT_API_CONSTRUCTIVE),
51
+ RoleName.ADVERSARIAL: (HF_MODEL_4B, HF_CHAT_API_ADVERSARIAL),
52
+ RoleName.EVIDENCE_BOUND: (HF_MODEL_8B, HF_CHAT_API_EVIDENCE),
53
+ }
54
+
55
+
56
  class HuggingFaceRoleBackend:
57
+ """Compatibility backend name retained; inference itself is local-only.
58
+
59
+ No Hugging Face Inference Provider endpoint is accepted. The HF token is
60
+ intentionally ignored for inference and exists only for old constructor compatibility.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ *,
66
+ role: RoleName,
67
+ model_id: str | None = None,
68
+ token: str = "",
69
+ api_url: str | None = None,
70
+ timeout_seconds: float = 600.0,
71
+ ):
72
+ expected_model, expected_url = _LOCAL_ROLE_CONFIG[role]
73
+ chosen_model = (model_id or expected_model).strip()
74
+ chosen_url = (api_url or expected_url).strip()
75
+ if chosen_model != expected_model:
76
+ raise ValueError(f"model_drift:{role.value}:{chosen_model}")
77
+ if chosen_url != expected_url:
78
+ raise ValueError(f"remote_or_wrong_endpoint_forbidden:{role.value}:{chosen_url}")
79
+ self.role = role
80
+ self.model_id = chosen_model
81
+ self.api_url = chosen_url
82
+ self.timeout_seconds = timeout_seconds
83
 
84
  async def generate_role(self, role: RoleName, packet: SharedRolePacket) -> RoleResult:
85
+ if role != self.role:
86
+ raise ValueError("backend_role_mismatch")
87
  prompt = {
88
  "role": role.value,
89
  "rules": role_rules(role),
90
  "packet": packet.model_dump(mode="json"),
91
  "required_output_schema": RoleResult.model_json_schema(),
92
+ "constraints": [
93
+ "Use only supplied packet facts for Astera-specific claims.",
94
+ "Return JSON only.",
95
+ "Do not claim external actions were executed.",
96
+ ],
97
  }
98
  async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
99
+ response = await client.post(
100
+ self.api_url,
101
+ json={
102
+ "model": self.model_id,
103
+ "messages": [{"role": "user", "content": json.dumps(prompt, ensure_ascii=False)}],
104
+ "response_format": {"type": "json_object"},
105
+ "stream": False,
106
+ },
107
+ )
108
  response.raise_for_status()
109
+ payload = response.json()
110
+ choices = payload.get("choices") or []
111
+ if not choices:
112
+ raise RuntimeError("model_empty_choices")
113
  content = choices[0].get("message", {}).get("content")
114
+ if not isinstance(content, str) or not content.strip():
115
+ raise RuntimeError("model_empty_content")
116
+ text = content.strip()
117
+ first = text.find("{")
118
+ last = text.rfind("}")
119
+ if first >= 0 and last >= first:
120
+ text = text[first : last + 1]
121
+ return RoleResult.model_validate_json(text)
runtime/runtime_factory.py CHANGED
@@ -4,7 +4,14 @@ from dataclasses import dataclass
4
 
5
  import httpx
6
 
7
- from .hf_client import HFChatClient, HF_CHAT_API, HF_MODEL_4B, HF_MODEL_8B
 
 
 
 
 
 
 
8
  from .integration import DialogueIntegrator
9
  from .internal_core import CustomerAIInternalCore
10
  from .kb_search import LocalHybridKnowledgeStore
@@ -32,8 +39,10 @@ class InternalRuntimeDependencies:
32
  constructive_model_id: str = HF_MODEL_4B
33
  adversarial_model_id: str = HF_MODEL_4B
34
  evidence_model_id: str = HF_MODEL_8B
35
- hf_api_url: str = HF_CHAT_API
36
- timeout_seconds: float = 30.0
 
 
37
 
38
 
39
  def _canonical_store(deps: InternalRuntimeDependencies):
@@ -56,27 +65,25 @@ def _role_pool(deps: InternalRuntimeDependencies):
56
  raise ValueError("adversarial_model_drift")
57
  if deps.evidence_model_id != HF_MODEL_8B:
58
  raise ValueError("evidence_model_drift")
59
- token = deps.hf_token or ""
60
- if not token.strip():
61
- raise ValueError("hf_token_required")
62
  shared_http = httpx.AsyncClient(timeout=deps.timeout_seconds)
63
  clients = {
64
  RoleName.CONSTRUCTIVE: HFChatClient(
65
- token=token,
66
  model_id=deps.constructive_model_id,
67
- api_url=deps.hf_api_url,
68
  client=shared_http,
69
  ),
70
  RoleName.ADVERSARIAL: HFChatClient(
71
- token=token,
72
  model_id=deps.adversarial_model_id,
73
- api_url=deps.hf_api_url,
74
  client=shared_http,
75
  ),
76
  RoleName.EVIDENCE_BOUND: HFChatClient(
77
- token=token,
78
  model_id=deps.evidence_model_id,
79
- api_url=deps.hf_api_url,
80
  client=shared_http,
81
  ),
82
  }
 
4
 
5
  import httpx
6
 
7
+ from .hf_client import (
8
+ HFChatClient,
9
+ HF_CHAT_API_ADVERSARIAL,
10
+ HF_CHAT_API_CONSTRUCTIVE,
11
+ HF_CHAT_API_EVIDENCE,
12
+ HF_MODEL_4B,
13
+ HF_MODEL_8B,
14
+ )
15
  from .integration import DialogueIntegrator
16
  from .internal_core import CustomerAIInternalCore
17
  from .kb_search import LocalHybridKnowledgeStore
 
39
  constructive_model_id: str = HF_MODEL_4B
40
  adversarial_model_id: str = HF_MODEL_4B
41
  evidence_model_id: str = HF_MODEL_8B
42
+ constructive_api_url: str = HF_CHAT_API_CONSTRUCTIVE
43
+ adversarial_api_url: str = HF_CHAT_API_ADVERSARIAL
44
+ evidence_api_url: str = HF_CHAT_API_EVIDENCE
45
+ timeout_seconds: float = 600.0
46
 
47
 
48
  def _canonical_store(deps: InternalRuntimeDependencies):
 
65
  raise ValueError("adversarial_model_drift")
66
  if deps.evidence_model_id != HF_MODEL_8B:
67
  raise ValueError("evidence_model_drift")
68
+
 
 
69
  shared_http = httpx.AsyncClient(timeout=deps.timeout_seconds)
70
  clients = {
71
  RoleName.CONSTRUCTIVE: HFChatClient(
72
+ token="",
73
  model_id=deps.constructive_model_id,
74
+ api_url=deps.constructive_api_url,
75
  client=shared_http,
76
  ),
77
  RoleName.ADVERSARIAL: HFChatClient(
78
+ token="",
79
  model_id=deps.adversarial_model_id,
80
+ api_url=deps.adversarial_api_url,
81
  client=shared_http,
82
  ),
83
  RoleName.EVIDENCE_BOUND: HFChatClient(
84
+ token="",
85
  model_id=deps.evidence_model_id,
86
+ api_url=deps.evidence_api_url,
87
  client=shared_http,
88
  ),
89
  }
runtime/startup.py CHANGED
@@ -6,7 +6,11 @@ from collections.abc import Iterable, Mapping
6
  from pathlib import Path
7
 
8
  from .bootstrap import RuntimeDependencies, build_work
9
- from .hf_client import HF_CHAT_API
 
 
 
 
10
  from .kb_bucket import (
11
  HF_KB_ACTIVE_POINTER_DEFAULT,
12
  HF_KB_BUCKET_DEFAULT,
@@ -59,10 +63,7 @@ def _load_alias_registry(path_value: str) -> Mapping[str, Iterable[str]]:
59
  def _production_kb_files(values: Mapping[str, str]) -> tuple[Path, Path | None, Path | None, str]:
60
  build_id = _required_value(values, "CUSTOMER_AI_KB_BUILD_ID", "kb_build_id_missing")
61
  mount_path = values.get("CUSTOMER_AI_KB_MOUNT_PATH", "").strip() or HF_KB_MOUNT_DEFAULT
62
- pointer_name = (
63
- values.get("CUSTOMER_AI_KB_ACTIVE_POINTER", "").strip()
64
- or HF_KB_ACTIVE_POINTER_DEFAULT
65
- )
66
  bucket_id = values.get("CUSTOMER_AI_KB_BUCKET_ID", "").strip() or HF_KB_BUCKET_DEFAULT
67
  token = (values.get("HF_TOKEN", "") or values.get("HF_KEY", "")).strip()
68
 
@@ -124,9 +125,9 @@ def create_work_from_environment(
124
  else:
125
  live_provider = EmptyLiveStateProvider()
126
 
 
 
127
  token = (values.get("HF_TOKEN", "") or values.get("HF_KEY", "")).strip()
128
- if role_pool is None and not token:
129
- raise RuntimeNotReady("hf_token_missing")
130
 
131
  try:
132
  fuzzy_threshold = float(values.get("CUSTOMER_AI_JA_FUZZY_THRESHOLD", "90"))
@@ -142,7 +143,12 @@ def create_work_from_environment(
142
  kb_generation_id=generation_id,
143
  hf_token=token or None,
144
  role_pool=role_pool,
145
- hf_api_url=values.get("CUSTOMER_AI_HF_API_URL", "").strip() or HF_CHAT_API,
146
- timeout_seconds=30.0,
 
 
 
 
 
147
  )
148
  )
 
6
  from pathlib import Path
7
 
8
  from .bootstrap import RuntimeDependencies, build_work
9
+ from .hf_client import (
10
+ HF_CHAT_API_ADVERSARIAL,
11
+ HF_CHAT_API_CONSTRUCTIVE,
12
+ HF_CHAT_API_EVIDENCE,
13
+ )
14
  from .kb_bucket import (
15
  HF_KB_ACTIVE_POINTER_DEFAULT,
16
  HF_KB_BUCKET_DEFAULT,
 
63
  def _production_kb_files(values: Mapping[str, str]) -> tuple[Path, Path | None, Path | None, str]:
64
  build_id = _required_value(values, "CUSTOMER_AI_KB_BUILD_ID", "kb_build_id_missing")
65
  mount_path = values.get("CUSTOMER_AI_KB_MOUNT_PATH", "").strip() or HF_KB_MOUNT_DEFAULT
66
+ pointer_name = values.get("CUSTOMER_AI_KB_ACTIVE_POINTER", "").strip() or HF_KB_ACTIVE_POINTER_DEFAULT
 
 
 
67
  bucket_id = values.get("CUSTOMER_AI_KB_BUCKET_ID", "").strip() or HF_KB_BUCKET_DEFAULT
68
  token = (values.get("HF_TOKEN", "") or values.get("HF_KEY", "")).strip()
69
 
 
125
  else:
126
  live_provider = EmptyLiveStateProvider()
127
 
128
+ # The HF token is needed only when the private KB bucket must be read remotely.
129
+ # Local role inference never uses the token.
130
  token = (values.get("HF_TOKEN", "") or values.get("HF_KEY", "")).strip()
 
 
131
 
132
  try:
133
  fuzzy_threshold = float(values.get("CUSTOMER_AI_JA_FUZZY_THRESHOLD", "90"))
 
143
  kb_generation_id=generation_id,
144
  hf_token=token or None,
145
  role_pool=role_pool,
146
+ constructive_api_url=values.get("CUSTOMER_AI_CONSTRUCTIVE_API_URL", "").strip()
147
+ or HF_CHAT_API_CONSTRUCTIVE,
148
+ adversarial_api_url=values.get("CUSTOMER_AI_ADVERSARIAL_API_URL", "").strip()
149
+ or HF_CHAT_API_ADVERSARIAL,
150
+ evidence_api_url=values.get("CUSTOMER_AI_EVIDENCE_API_URL", "").strip()
151
+ or HF_CHAT_API_EVIDENCE,
152
+ timeout_seconds=float(values.get("CUSTOMER_AI_ROLE_TIMEOUT_SECONDS", "600")),
153
  )
154
  )
scripts/start_local_cpu.sh ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ MODEL_4B="${CUSTOMER_AI_LOCAL_4B_PATH:-/models/llm-jp-3-3.7b-instruct3-Q4_K_M.gguf}"
5
+ MODEL_8B="${CUSTOMER_AI_LOCAL_8B_PATH:-/models/llm-jp-4-8b-instruct-Q4_K_M.gguf}"
6
+
7
+ for path in "$MODEL_4B" "$MODEL_8B"; do
8
+ if [ ! -s "$path" ]; then
9
+ echo "local_model_missing=$path" >&2
10
+ exit 1
11
+ fi
12
+ done
13
+
14
+ start_server() {
15
+ name="$1"
16
+ model="$2"
17
+ alias="$3"
18
+ port="$4"
19
+ threads="$5"
20
+
21
+ /opt/llama/llama-server \
22
+ --model "$model" \
23
+ --alias "$alias" \
24
+ --host 127.0.0.1 \
25
+ --port "$port" \
26
+ --ctx-size 2048 \
27
+ --threads "$threads" \
28
+ --threads-batch "$threads" \
29
+ --parallel 1 \
30
+ --jinja \
31
+ > "/tmp/${name}.log" 2>&1 &
32
+ echo $!
33
+ }
34
+
35
+ # Required topology: two independent 4B runtimes plus one independent 8B runtime.
36
+ PID_CONSTRUCTIVE=$(start_server constructive "$MODEL_4B" "llm-jp/llm-jp-3-3.7b-instruct3" 8081 1)
37
+ PID_ADVERSARIAL=$(start_server adversarial "$MODEL_4B" "llm-jp/llm-jp-3-3.7b-instruct3" 8082 1)
38
+ PID_EVIDENCE=$(start_server evidence "$MODEL_8B" "llm-jp/llm-jp-4-8b-instruct" 8083 1)
39
+
40
+ check_alive() {
41
+ pid="$1"
42
+ name="$2"
43
+ if ! kill -0 "$pid" 2>/dev/null; then
44
+ echo "local_llama_server_exited=$name" >&2
45
+ tail -200 "/tmp/${name}.log" >&2 || true
46
+ exit 1
47
+ fi
48
+ }
49
+
50
+ ready=0
51
+ i=0
52
+ while [ "$i" -lt 900 ]; do
53
+ check_alive "$PID_CONSTRUCTIVE" constructive
54
+ check_alive "$PID_ADVERSARIAL" adversarial
55
+ check_alive "$PID_EVIDENCE" evidence
56
+ if curl -fsS http://127.0.0.1:8081/health >/dev/null 2>&1 \
57
+ && curl -fsS http://127.0.0.1:8082/health >/dev/null 2>&1 \
58
+ && curl -fsS http://127.0.0.1:8083/health >/dev/null 2>&1; then
59
+ ready=1
60
+ break
61
+ fi
62
+ i=$((i + 1))
63
+ sleep 1
64
+ done
65
+
66
+ if [ "$ready" -ne 1 ]; then
67
+ echo "local_role_models_not_ready" >&2
68
+ for name in constructive adversarial evidence; do
69
+ echo "--- ${name} ---" >&2
70
+ tail -100 "/tmp/${name}.log" >&2 || true
71
+ done
72
+ exit 1
73
+ fi
74
+
75
+ echo "LOCAL_ROLE_MODELS_READY=constructive:4B@8081,adversarial:4B@8082,evidence:8B@8083,provider:none"
76
+ exec uvicorn app:app --host 0.0.0.0 --port "${PORT:-7860}"