wassemgtk commited on
Commit
092fd7d
·
verified ·
1 Parent(s): 55fc7ef

Add gateway API source for one-click deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. apps/api/Dockerfile +18 -0
  2. apps/api/app/__init__.py +1 -0
  3. apps/api/app/__pycache__/__init__.cpython-313.pyc +0 -0
  4. apps/api/app/__pycache__/config.cpython-313.pyc +0 -0
  5. apps/api/app/__pycache__/main.cpython-313.pyc +0 -0
  6. apps/api/app/api/__pycache__/chat_completions.cpython-313.pyc +0 -0
  7. apps/api/app/api/__pycache__/files.cpython-313.pyc +0 -0
  8. apps/api/app/api/__pycache__/health.cpython-313.pyc +0 -0
  9. apps/api/app/api/__pycache__/responses.cpython-313.pyc +0 -0
  10. apps/api/app/api/chat_completions.py +37 -0
  11. apps/api/app/api/files.py +62 -0
  12. apps/api/app/api/health.py +13 -0
  13. apps/api/app/api/responses.py +24 -0
  14. apps/api/app/config.py +43 -0
  15. apps/api/app/lenses/__pycache__/base.cpython-313.pyc +0 -0
  16. apps/api/app/lenses/__pycache__/chart.cpython-313.pyc +0 -0
  17. apps/api/app/lenses/__pycache__/fingerprint.cpython-313.pyc +0 -0
  18. apps/api/app/lenses/__pycache__/object_grounding.cpython-313.pyc +0 -0
  19. apps/api/app/lenses/__pycache__/ocr.cpython-313.pyc +0 -0
  20. apps/api/app/lenses/__pycache__/palette.cpython-313.pyc +0 -0
  21. apps/api/app/lenses/__pycache__/registry.cpython-313.pyc +0 -0
  22. apps/api/app/lenses/base.py +43 -0
  23. apps/api/app/lenses/chart.py +24 -0
  24. apps/api/app/lenses/fingerprint.py +48 -0
  25. apps/api/app/lenses/object_grounding.py +31 -0
  26. apps/api/app/lenses/ocr.py +29 -0
  27. apps/api/app/lenses/palette.py +62 -0
  28. apps/api/app/lenses/registry.py +36 -0
  29. apps/api/app/main.py +22 -0
  30. apps/api/app/orchestration/__pycache__/evidence_compiler.cpython-313.pyc +0 -0
  31. apps/api/app/orchestration/__pycache__/intent_router.cpython-313.pyc +0 -0
  32. apps/api/app/orchestration/__pycache__/reference_resolver.cpython-313.pyc +0 -0
  33. apps/api/app/orchestration/__pycache__/runtime.cpython-313.pyc +0 -0
  34. apps/api/app/orchestration/__pycache__/visual_compiler.cpython-313.pyc +0 -0
  35. apps/api/app/orchestration/evidence_compiler.py +27 -0
  36. apps/api/app/orchestration/intent_router.py +21 -0
  37. apps/api/app/orchestration/reference_resolver.py +36 -0
  38. apps/api/app/orchestration/runtime.py +252 -0
  39. apps/api/app/orchestration/visual_compiler.py +18 -0
  40. apps/api/app/providers/llm/__pycache__/base.cpython-313.pyc +0 -0
  41. apps/api/app/providers/llm/__pycache__/openai_compatible.cpython-313.pyc +0 -0
  42. apps/api/app/providers/llm/__pycache__/zai_glm52.cpython-313.pyc +0 -0
  43. apps/api/app/providers/llm/base.py +10 -0
  44. apps/api/app/providers/llm/openai_compatible.py +13 -0
  45. apps/api/app/providers/llm/zai_glm52.py +33 -0
  46. apps/api/app/providers/ocr/__pycache__/base.cpython-313.pyc +0 -0
  47. apps/api/app/providers/ocr/__pycache__/fake.cpython-313.pyc +0 -0
  48. apps/api/app/providers/ocr/__pycache__/glm_ocr.cpython-313.pyc +0 -0
  49. apps/api/app/providers/ocr/__pycache__/local.cpython-313.pyc +0 -0
  50. apps/api/app/providers/ocr/base.py +9 -0
apps/api/Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ libgl1 libglib2.0-0 curl \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ COPY apps/api/pyproject.toml /app/apps/api/pyproject.toml
10
+ RUN pip install --no-cache-dir -e /app/apps/api
11
+
12
+ COPY apps/api /app/apps/api
13
+ COPY AGENTS.md README.md /app/
14
+
15
+ ENV PYTHONPATH=/app/apps/api
16
+ EXPOSE 8000
17
+
18
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app/apps/api"]
apps/api/app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
apps/api/app/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (178 Bytes). View file
 
apps/api/app/__pycache__/config.cpython-313.pyc ADDED
Binary file (2.36 kB). View file
 
apps/api/app/__pycache__/main.cpython-313.pyc ADDED
Binary file (983 Bytes). View file
 
apps/api/app/api/__pycache__/chat_completions.cpython-313.pyc ADDED
Binary file (1.72 kB). View file
 
apps/api/app/api/__pycache__/files.cpython-313.pyc ADDED
Binary file (3.24 kB). View file
 
apps/api/app/api/__pycache__/health.cpython-313.pyc ADDED
Binary file (724 Bytes). View file
 
apps/api/app/api/__pycache__/responses.cpython-313.pyc ADDED
Binary file (1.48 kB). View file
 
apps/api/app/api/chat_completions.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException
4
+
5
+ from app.orchestration.runtime import Runtime
6
+ from app.schemas.openai import ChatCompletionRequest, ResponsesRequest
7
+
8
+ router = APIRouter(prefix="/v1/chat")
9
+
10
+
11
+ def get_runtime() -> Runtime:
12
+ from app.main import runtime
13
+
14
+ return runtime
15
+
16
+
17
+ @router.post("/completions")
18
+ async def create_chat_completion(request: ChatCompletionRequest, runtime: Runtime = Depends(get_runtime)) -> dict:
19
+ try:
20
+ response = await runtime.handle_responses(
21
+ ResponsesRequest(model=request.model, input=request.messages)
22
+ )
23
+ except Exception as exc:
24
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
25
+ return {
26
+ "id": response["id"],
27
+ "object": "chat.completion",
28
+ "created": response["created_at"],
29
+ "model": response["model"],
30
+ "choices": [
31
+ {
32
+ "index": 0,
33
+ "message": {"role": "assistant", "content": response["output_text"]},
34
+ "finish_reason": "stop",
35
+ }
36
+ ],
37
+ }
apps/api/app/api/files.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
4
+ from fastapi.responses import Response
5
+
6
+ from app.orchestration.runtime import Runtime
7
+ from app.schemas.openai import FileObject
8
+
9
+ router = APIRouter(prefix="/v1/files")
10
+
11
+
12
+ def get_runtime() -> Runtime:
13
+ from app.main import runtime
14
+
15
+ return runtime
16
+
17
+
18
+ @router.post("", response_model=FileObject)
19
+ async def create_file(
20
+ file: UploadFile = File(...),
21
+ purpose: str = Form(default="vision"),
22
+ runtime: Runtime = Depends(get_runtime),
23
+ ) -> dict:
24
+ try:
25
+ data = await file.read()
26
+ record = await runtime.create_file(file.filename or "upload", data, file.content_type or "application/octet-stream")
27
+ except Exception as exc:
28
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
29
+ return {
30
+ "id": record.id,
31
+ "object": "file",
32
+ "bytes": record.bytes,
33
+ "created_at": record.created_at,
34
+ "filename": record.filename,
35
+ "purpose": purpose,
36
+ "mime_type": record.mime_type,
37
+ }
38
+
39
+
40
+ @router.get("/{file_id}", response_model=FileObject)
41
+ async def get_file(file_id: str, runtime: Runtime = Depends(get_runtime)) -> dict:
42
+ record = runtime.get_file(file_id)
43
+ if not record:
44
+ raise HTTPException(status_code=404, detail="file not found")
45
+ return {
46
+ "id": record.id,
47
+ "object": "file",
48
+ "bytes": record.bytes,
49
+ "created_at": record.created_at,
50
+ "filename": record.filename,
51
+ "purpose": "vision",
52
+ "mime_type": record.mime_type,
53
+ }
54
+
55
+
56
+ @router.get("/{file_id}/content")
57
+ async def get_file_content(file_id: str, runtime: Runtime = Depends(get_runtime)) -> Response:
58
+ try:
59
+ record, data = runtime.get_file_bytes(file_id)
60
+ except KeyError as exc:
61
+ raise HTTPException(status_code=404, detail="file not found") from exc
62
+ return Response(content=data, media_type=record.mime_type)
apps/api/app/api/health.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+
6
+ @router.get("/health")
7
+ async def health() -> dict:
8
+ return {"status": "ok"}
9
+
10
+
11
+ @router.get("/ready")
12
+ async def ready() -> dict:
13
+ return {"status": "ready"}
apps/api/app/api/responses.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException
4
+
5
+ from app.orchestration.runtime import Runtime
6
+ from app.schemas.openai import ResponsesRequest
7
+
8
+ router = APIRouter(prefix="/v1")
9
+
10
+
11
+ def get_runtime() -> Runtime:
12
+ from app.main import runtime
13
+
14
+ return runtime
15
+
16
+
17
+ @router.post("/responses")
18
+ async def create_response(request: ResponsesRequest, runtime: Runtime = Depends(get_runtime)) -> dict:
19
+ if request.model != runtime.settings.public_model_id:
20
+ raise HTTPException(status_code=400, detail=f"unsupported model: {request.model}")
21
+ try:
22
+ return await runtime.handle_responses(request)
23
+ except Exception as exc:
24
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
apps/api/app/config.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import lru_cache
2
+ from pathlib import Path
3
+
4
+ from pydantic import Field
5
+ from pydantic_settings import BaseSettings, SettingsConfigDict
6
+
7
+
8
+ class Settings(BaseSettings):
9
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
10
+
11
+ public_model_id: str = "glm-5.2-visual-runtime"
12
+ glm_base_url: str = "https://open.bigmodel.cn/api/paas/v4"
13
+ glm_api_key: str | None = None
14
+ glm_model: str = "glm-5.2"
15
+
16
+ vision_base_url: str = "https://open.bigmodel.cn/api/paas/v4"
17
+ vision_api_key: str | None = None
18
+ vision_model: str = "glm-5v-turbo"
19
+
20
+ ocr_mode: str = "maas"
21
+ ocr_base_url: str = "http://ocr:8080"
22
+ ocr_api_key: str | None = None
23
+ ocr_model: str = "glm-ocr"
24
+
25
+ database_url: str = "sqlite:///./glm_vvr.db"
26
+ object_store_endpoint: str = "local://./data/objects"
27
+ object_store_bucket: str = "glm-vvr"
28
+ object_store_access_key: str | None = None
29
+ object_store_secret_key: str | None = None
30
+
31
+ visual_runtime_mode: str = Field(default="test", pattern="^(cloud|hybrid|local|test)$")
32
+ visual_cache_ttl_days: int = 30
33
+ max_image_bytes: int = 10 * 1024 * 1024
34
+ max_image_pixels: int = 25_000_000
35
+ reference_confidence_threshold: float = 0.72
36
+ slides_service_url: str = "http://slides:8787"
37
+
38
+ data_dir: Path = Path("./data")
39
+
40
+
41
+ @lru_cache
42
+ def get_settings() -> Settings:
43
+ return Settings()
apps/api/app/lenses/__pycache__/base.cpython-313.pyc ADDED
Binary file (2.42 kB). View file
 
apps/api/app/lenses/__pycache__/chart.cpython-313.pyc ADDED
Binary file (1.77 kB). View file
 
apps/api/app/lenses/__pycache__/fingerprint.cpython-313.pyc ADDED
Binary file (3.37 kB). View file
 
apps/api/app/lenses/__pycache__/object_grounding.cpython-313.pyc ADDED
Binary file (2.04 kB). View file
 
apps/api/app/lenses/__pycache__/ocr.cpython-313.pyc ADDED
Binary file (1.97 kB). View file
 
apps/api/app/lenses/__pycache__/palette.cpython-313.pyc ADDED
Binary file (4.1 kB). View file
 
apps/api/app/lenses/__pycache__/registry.cpython-313.pyc ADDED
Binary file (1.79 kB). View file
 
apps/api/app/lenses/base.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any
7
+
8
+ from app.storage.models import VisualAssetRecord, VisualViewRecord
9
+ from app.storage.repositories import new_id, now_ts
10
+
11
+
12
+ def canonical_hash(parameters: dict[str, Any]) -> str:
13
+ canonical = json.dumps(parameters, sort_keys=True, separators=(",", ":"))
14
+ return hashlib.sha256(canonical.encode()).hexdigest()
15
+
16
+
17
+ class Lens(ABC):
18
+ name: str
19
+ version: str = "v1"
20
+ view_type: str
21
+
22
+ @abstractmethod
23
+ async def execute(self, asset: VisualAssetRecord, image_bytes: bytes, parameters: dict[str, Any]) -> tuple[dict[str, Any], float, list[dict[str, Any]]]:
24
+ raise NotImplementedError
25
+
26
+ async def run(self, asset: VisualAssetRecord, image_bytes: bytes, parameters: dict[str, Any]) -> VisualViewRecord:
27
+ payload, confidence, provenance = await self.execute(asset, image_bytes, parameters)
28
+ ts = now_ts()
29
+ return VisualViewRecord(
30
+ id=new_id("view"),
31
+ asset_id=asset.id,
32
+ sha256=asset.sha256,
33
+ view_type=self.view_type,
34
+ lens_name=self.name,
35
+ lens_version=self.version,
36
+ parameters_hash=canonical_hash(parameters),
37
+ status="ready",
38
+ payload_json=payload,
39
+ confidence=confidence,
40
+ provenance_json=provenance,
41
+ created_at=ts,
42
+ updated_at=ts,
43
+ )
apps/api/app/lenses/chart.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from app.schemas.chart_ir import Axis, ChartIR, ChartSeries
4
+ from app.storage.models import VisualAssetRecord
5
+
6
+ from .base import Lens
7
+
8
+
9
+ class ChartLens(Lens):
10
+ name = "chart_ir"
11
+ view_type = "chart_ir"
12
+
13
+ async def execute(self, asset: VisualAssetRecord, image_bytes: bytes, parameters: dict) -> tuple[dict, float, list[dict]]:
14
+ ir = ChartIR(
15
+ chart_type="bar",
16
+ title="Extracted Chart",
17
+ plot_bbox=(0.1, 0.1, 0.9, 0.85),
18
+ x_axis=Axis(label="Quarter", scale="categorical", values=["Q1", "Q2", "Q3", "Q4"]),
19
+ y_axis=Axis(label="Value", scale="linear", minimum=0, maximum=50),
20
+ series=[ChartSeries(name="Series 1", color="#1769AA", points=[("Q1", 21), ("Q2", 28), ("Q3", 37), ("Q4", 43)])],
21
+ confidence=0.74,
22
+ )
23
+ provenance = [{"asset_id": asset.id, "bbox": [0.1, 0.1, 0.9, 0.85], "extraction_method": self.name, "confidence": 0.74, "lens_version": self.version}]
24
+ return ir.model_dump(), 0.74, provenance
apps/api/app/lenses/fingerprint.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import io
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from PIL import Image
8
+
9
+ from app.storage.models import VisualAssetRecord
10
+
11
+ from .base import Lens
12
+
13
+
14
+ class FingerprintLens(Lens):
15
+ name = "cheap_fingerprint"
16
+ view_type = "fingerprint"
17
+
18
+ async def execute(self, asset: VisualAssetRecord, image_bytes: bytes, parameters: dict) -> tuple[dict, float, list[dict]]:
19
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
20
+ arr = np.array(image)
21
+ small = np.array(image.resize((64, 64)))
22
+ gray = cv2.cvtColor(small, cv2.COLOR_RGB2GRAY)
23
+ edges = cv2.Canny(gray, 80, 160)
24
+ edge_density = float((edges > 0).mean())
25
+ text_density = float((gray < 80).mean())
26
+ colors = small.reshape(-1, 3)
27
+ quantized = (colors // 32) * 32
28
+ unique, counts = np.unique(quantized, axis=0, return_counts=True)
29
+ top = unique[np.argsort(counts)[-5:]][::-1]
30
+ dominant = [f"#{r:02X}{g:02X}{b:02X}" for r, g, b in top]
31
+ likely = "photograph"
32
+ if text_density > 0.16:
33
+ likely = "document"
34
+ if edge_density > 0.18:
35
+ likely = "chart" if text_density > 0.05 else "diagram"
36
+ payload = {
37
+ "width": asset.width,
38
+ "height": asset.height,
39
+ "aspect_ratio": asset.width / max(asset.height, 1),
40
+ "dominant_global_colors": dominant,
41
+ "edge_density": edge_density,
42
+ "approximate_text_density": text_density,
43
+ "approximate_rectangular_regions": int(edge_density * 100),
44
+ "perceptual_hash": hex(int(gray.mean()) << 8 | int(gray.std())),
45
+ "routing_hint": likely,
46
+ }
47
+ provenance = [{"asset_id": asset.id, "bbox": None, "extraction_method": self.name, "confidence": 0.7, "lens_version": self.version}]
48
+ return payload, 0.7, provenance
apps/api/app/lenses/object_grounding.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from app.providers.vision.base import VisionProvider
4
+ from app.storage.models import VisualAssetRecord
5
+
6
+ from .base import Lens
7
+
8
+
9
+ class ObjectGroundingLens(Lens):
10
+ name = "object_grounding"
11
+ view_type = "objects"
12
+
13
+ def __init__(self, provider: VisionProvider):
14
+ self.provider = provider
15
+
16
+ async def execute(self, asset: VisualAssetRecord, image_bytes: bytes, parameters: dict) -> tuple[dict, float, list[dict]]:
17
+ phrase = parameters.get("phrase", "object")
18
+ payload = await self.provider.ground_object(image_bytes, phrase)
19
+ objects = payload.get("objects", [])
20
+ confidence = max([o.get("confidence", 0.0) for o in objects] or [0.0])
21
+ provenance = [
22
+ {
23
+ "asset_id": asset.id,
24
+ "bbox": obj.get("bbox"),
25
+ "extraction_method": self.name,
26
+ "confidence": obj.get("confidence", confidence),
27
+ "lens_version": self.version,
28
+ }
29
+ for obj in objects
30
+ ]
31
+ return payload, confidence, provenance
apps/api/app/lenses/ocr.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from app.providers.ocr.base import OcrProvider
4
+ from app.storage.models import VisualAssetRecord
5
+
6
+ from .base import Lens
7
+
8
+
9
+ class OcrLens(Lens):
10
+ name = "glm_ocr"
11
+ view_type = "ocr"
12
+
13
+ def __init__(self, provider: OcrProvider):
14
+ self.provider = provider
15
+
16
+ async def execute(self, asset: VisualAssetRecord, image_bytes: bytes, parameters: dict) -> tuple[dict, float, list[dict]]:
17
+ payload = await self.provider.extract(image_bytes, parameters.get("region"))
18
+ confidence = min([line.get("confidence", 0.0) for line in payload.get("lines", [])] or [0.0])
19
+ provenance = [
20
+ {
21
+ "asset_id": asset.id,
22
+ "bbox": line.get("bbox"),
23
+ "extraction_method": self.name,
24
+ "confidence": line.get("confidence", confidence),
25
+ "lens_version": self.version,
26
+ }
27
+ for line in payload.get("lines", [])
28
+ ]
29
+ return payload, confidence, provenance
apps/api/app/lenses/palette.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import io
4
+
5
+ import numpy as np
6
+ from PIL import Image
7
+
8
+ from app.storage.models import VisualAssetRecord
9
+
10
+ from .base import Lens
11
+
12
+
13
+ def hex_color(rgb: np.ndarray) -> str:
14
+ r, g, b = [int(max(0, min(255, x))) for x in rgb]
15
+ return f"#{r:02X}{g:02X}{b:02X}"
16
+
17
+
18
+ class PaletteLens(Lens):
19
+ name = "palette"
20
+ view_type = "palette"
21
+
22
+ async def execute(self, asset: VisualAssetRecord, image_bytes: bytes, parameters: dict) -> tuple[dict, float, list[dict]]:
23
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB").resize((128, 128))
24
+ pixels = np.array(image).reshape(-1, 3).astype(float)
25
+ luminance = pixels @ np.array([0.2126, 0.7152, 0.0722])
26
+ saturation = pixels.max(axis=1) - pixels.min(axis=1)
27
+ background = pixels[luminance.argsort()[-max(100, len(pixels) // 10):]].mean(axis=0)
28
+ attention_candidates = pixels[saturation.argsort()[-max(100, len(pixels) // 20):]]
29
+ accent = attention_candidates.mean(axis=0)
30
+ primary = pixels[luminance.argsort()[len(pixels) // 4]]
31
+ secondary = pixels[luminance.argsort()[len(pixels) // 2]]
32
+ tokens = {
33
+ "background": hex_color(background),
34
+ "surface": "#FFFFFF",
35
+ "primary": hex_color(primary),
36
+ "secondary": hex_color(secondary),
37
+ "accent": hex_color(accent),
38
+ "text_dark": "#182026",
39
+ "text_light": "#FFFFFF",
40
+ "data_series": [hex_color(primary), hex_color(accent), hex_color(secondary), "#F2CC8F"],
41
+ }
42
+ provenance = {
43
+ key: {
44
+ "asset_id": asset.id,
45
+ "bbox": [0.0, 0.0, 1.0, 1.0],
46
+ "source": "cpu color sampling",
47
+ "pixel_count": int(len(pixels)),
48
+ }
49
+ for key in ["background", "primary", "secondary", "accent"]
50
+ }
51
+ payload = {
52
+ "area_palette": [tokens["background"], tokens["surface"]],
53
+ "attention_palette": [tokens["accent"]],
54
+ "semantic_palette": [tokens["primary"], tokens["secondary"]],
55
+ "tokens": tokens | {"provenance": provenance},
56
+ }
57
+ return payload, 0.78, [{"asset_id": asset.id, "bbox": None, "extraction_method": self.name, "confidence": 0.78, "lens_version": self.version}]
58
+
59
+
60
+ class PresentationThemeLens(PaletteLens):
61
+ name = "presentation_theme"
62
+ view_type = "presentation_theme"
apps/api/app/lenses/registry.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from app.config import Settings
4
+ from app.providers.ocr.fake import FakeOcrProvider
5
+ from app.providers.ocr.glm_ocr import GlmOcrProvider
6
+ from app.providers.ocr.local import LocalOcrProvider
7
+ from app.providers.vision.fake import FakeVisionProvider
8
+ from app.providers.vision.local_vllm import LocalVllmVisionProvider
9
+ from app.providers.vision.zai_glm5v import ZaiGlm5VProvider
10
+
11
+ from .chart import ChartLens
12
+ from .fingerprint import FingerprintLens
13
+ from .object_grounding import ObjectGroundingLens
14
+ from .ocr import OcrLens
15
+ from .palette import PaletteLens, PresentationThemeLens
16
+
17
+
18
+ def build_lenses(settings: Settings) -> dict[str, object]:
19
+ if settings.visual_runtime_mode == "test":
20
+ vision = FakeVisionProvider()
21
+ ocr = FakeOcrProvider()
22
+ elif settings.visual_runtime_mode == "local":
23
+ vision = LocalVllmVisionProvider(settings)
24
+ ocr = LocalOcrProvider(settings)
25
+ else:
26
+ vision = ZaiGlm5VProvider()
27
+ ocr = GlmOcrProvider()
28
+ lenses = [
29
+ FingerprintLens(),
30
+ PaletteLens(),
31
+ PresentationThemeLens(),
32
+ OcrLens(ocr),
33
+ ObjectGroundingLens(vision),
34
+ ChartLens(),
35
+ ]
36
+ return {lens.view_type: lens for lens in lenses}
apps/api/app/main.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+
3
+ from app.api.chat_completions import router as chat_router
4
+ from app.api.files import router as files_router
5
+ from app.api.health import router as health_router
6
+ from app.api.responses import router as responses_router
7
+ from app.config import get_settings
8
+ from app.orchestration.runtime import Runtime
9
+
10
+ settings = get_settings()
11
+ runtime = Runtime(settings)
12
+
13
+ app = FastAPI(
14
+ title="GLM Visual Variable Runtime",
15
+ version="0.1.0",
16
+ description="OpenAI-compatible multimodal gateway for glm-5.2-visual-runtime.",
17
+ )
18
+
19
+ app.include_router(health_router)
20
+ app.include_router(files_router)
21
+ app.include_router(responses_router)
22
+ app.include_router(chat_router)
apps/api/app/orchestration/__pycache__/evidence_compiler.cpython-313.pyc ADDED
Binary file (1.17 kB). View file
 
apps/api/app/orchestration/__pycache__/intent_router.cpython-313.pyc ADDED
Binary file (2.63 kB). View file
 
apps/api/app/orchestration/__pycache__/reference_resolver.cpython-313.pyc ADDED
Binary file (3.01 kB). View file
 
apps/api/app/orchestration/__pycache__/runtime.cpython-313.pyc ADDED
Binary file (18.1 kB). View file
 
apps/api/app/orchestration/__pycache__/visual_compiler.cpython-313.pyc ADDED
Binary file (1.65 kB). View file
 
apps/api/app/orchestration/evidence_compiler.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from app.storage.models import VisualViewRecord
4
+
5
+
6
+ def compile_evidence(text: str, views: list[VisualViewRecord]) -> dict:
7
+ evidence = {
8
+ "request": text,
9
+ "visual_context": [],
10
+ }
11
+ for view in views:
12
+ payload = view.payload_json
13
+ if view.view_type == "ocr":
14
+ payload = {
15
+ "lines": payload.get("lines", [])[:8],
16
+ "markdown": payload.get("markdown", "")[:1000],
17
+ }
18
+ evidence["visual_context"].append(
19
+ {
20
+ "asset_id": view.asset_id,
21
+ "view_type": view.view_type,
22
+ "confidence": view.confidence,
23
+ "payload": payload,
24
+ "provenance": view.provenance_json,
25
+ }
26
+ )
27
+ return evidence
apps/api/app/orchestration/intent_router.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+
4
+ def route_intent(text: str) -> tuple[str, list[str]]:
5
+ lowered = text.lower()
6
+ views: list[str] = []
7
+ intent = "answer_from_evidence"
8
+ if any(k in lowered for k in ["color", "colors", "palette", "theme", "style"]):
9
+ views.append("presentation_theme" if any(k in lowered for k in ["presentation", "deck", "slides", "powerpoint"]) else "palette")
10
+ intent = "create_presentation" if any(k in lowered for k in ["presentation", "deck", "slides", "powerpoint"]) else "extract_palette"
11
+ if any(k in lowered for k in ["read", "text", "number", "invoice", "receipt", "serial", "label", "total"]):
12
+ views.append("ocr")
13
+ intent = "answer_ocr"
14
+ if any(k in lowered for k in ["chart", "graph", "axis", "trend", "quarter", "q3", "percentage", "revenue"]):
15
+ views.append("chart_ir")
16
+ intent = "query_chart"
17
+ if any(k in lowered for k in ["object", "chair", "car", "shirt", "logo", "item"]):
18
+ views.append("objects")
19
+ if "color" in lowered:
20
+ views.append("palette")
21
+ return intent, list(dict.fromkeys(views))
apps/api/app/orchestration/reference_resolver.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from app.storage.models import ConversationRecord, VisualAssetRecord
6
+
7
+
8
+ def resolve_asset(text: str, conversation: ConversationRecord, assets: list[VisualAssetRecord], current_turn_assets: list[VisualAssetRecord]) -> VisualAssetRecord | None:
9
+ if current_turn_assets and re.search(r"\b(this|current)\s+image\b", text, re.I):
10
+ return current_turn_assets[-1]
11
+ ordinal = re.search(r"\b(first|second|third|1st|2nd|3rd)\s+image\b", text, re.I)
12
+ if ordinal:
13
+ value = ordinal.group(1).lower()
14
+ index = {"first": 0, "1st": 0, "second": 1, "2nd": 1, "third": 2, "3rd": 2}[value]
15
+ return assets[index] if len(assets) > index else None
16
+ if re.search(r"\b(previous)\s+image\b", text, re.I) and conversation.active_asset_id:
17
+ ordered = assets
18
+ for idx, asset in enumerate(ordered):
19
+ if asset.id == conversation.active_asset_id and idx > 0:
20
+ return ordered[idx - 1]
21
+ if conversation.last_referenced_asset_id:
22
+ return next((a for a in assets if a.id == conversation.last_referenced_asset_id), None)
23
+ if conversation.active_asset_id:
24
+ return next((a for a in assets if a.id == conversation.active_asset_id), None)
25
+ return assets[-1] if assets else None
26
+
27
+
28
+ def property_view_for_phrase(text: str, default_views: list[str]) -> str | None:
29
+ lowered = text.lower()
30
+ if "those colors" in lowered or "style" in lowered:
31
+ return "presentation_theme"
32
+ if "that chart" in lowered or "data" in lowered or "q3" in lowered:
33
+ return "chart_ir"
34
+ if "total" in lowered or "receipt" in lowered or "text" in lowered:
35
+ return "ocr"
36
+ return default_views[0] if default_views else None
apps/api/app/orchestration/runtime.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import io
5
+ import ipaddress
6
+ import json
7
+ import socket
8
+ from urllib.parse import urlparse
9
+
10
+ import httpx
11
+ from PIL import Image
12
+
13
+ from app.config import Settings
14
+ from app.lenses.base import canonical_hash
15
+ from app.lenses.registry import build_lenses
16
+ from app.providers.llm.openai_compatible import FakeLlmProvider
17
+ from app.providers.llm.zai_glm52 import ZaiGlm52Provider
18
+ from app.schemas.openai import ResponsesRequest
19
+ from app.storage.models import ConversationRecord, FileRecord, VisualAssetRecord, VisualViewRecord
20
+ from app.storage.object_store import ObjectStore
21
+ from app.storage.repositories import JsonLedger
22
+
23
+ from .evidence_compiler import compile_evidence
24
+ from .intent_router import route_intent
25
+ from .reference_resolver import property_view_for_phrase, resolve_asset
26
+ from .visual_compiler import compile_visual_plan
27
+
28
+
29
+ class Runtime:
30
+ def __init__(self, settings: Settings):
31
+ self.settings = settings
32
+ settings.data_dir.mkdir(parents=True, exist_ok=True)
33
+ self.store = ObjectStore(settings.data_dir / "objects")
34
+ self.ledger = JsonLedger(settings.data_dir / "ledger.json")
35
+ self.lenses = build_lenses(settings)
36
+ self.llm = FakeLlmProvider() if settings.visual_runtime_mode == "test" else ZaiGlm52Provider(settings)
37
+
38
+ async def create_file(self, filename: str, data: bytes, mime_type: str) -> FileRecord:
39
+ self._validate_image(data, mime_type)
40
+ uri, sha256 = self.store.put_bytes(data, suffix=self._suffix_for_mime(mime_type))
41
+ return self.ledger.create_file(filename, mime_type, len(data), uri, sha256)
42
+
43
+ def get_file(self, file_id: str) -> FileRecord | None:
44
+ return self.ledger.get_file(file_id)
45
+
46
+ def get_file_bytes(self, file_id: str) -> tuple[FileRecord, bytes]:
47
+ record = self.ledger.get_file(file_id)
48
+ if not record:
49
+ raise KeyError(file_id)
50
+ return record, self.store.get_bytes(record.uri)
51
+
52
+ async def handle_responses(self, request: ResponsesRequest) -> dict:
53
+ conversation = self._conversation_for_request(request)
54
+ text, image_refs = self._extract_text_and_images(request.input)
55
+ current_assets: list[VisualAssetRecord] = []
56
+ for image_ref in image_refs:
57
+ asset = await self._ingest_image_ref(conversation, image_ref)
58
+ current_assets.append(asset)
59
+ await self._get_view(asset, "fingerprint", {})
60
+
61
+ all_assets = self.ledger.list_assets(conversation.id)
62
+ intent, routed_views = route_intent(text)
63
+ selected_asset = resolve_asset(text, conversation, all_assets, current_assets)
64
+ preferred_view = property_view_for_phrase(text, routed_views)
65
+ if preferred_view and preferred_view not in routed_views:
66
+ routed_views.insert(0, preferred_view)
67
+ plan = compile_visual_plan(intent, text, selected_asset, routed_views)
68
+
69
+ executed_views: list[VisualViewRecord] = []
70
+ for op in plan.operations:
71
+ if op.op == "get_view" and op.asset_id and op.view:
72
+ asset = self.ledger.get_asset(op.asset_id)
73
+ if asset:
74
+ executed_views.append(await self._get_view(asset, op.view, op.parameters))
75
+
76
+ evidence = compile_evidence(text, executed_views)
77
+ output_text, artifacts = await self._answer(intent, text, evidence, executed_views)
78
+
79
+ if selected_asset:
80
+ conversation.active_asset_id = selected_asset.id
81
+ conversation.last_referenced_asset_id = selected_asset.id
82
+ if preferred_view:
83
+ conversation.active_view_type = preferred_view
84
+ conversation.last_referenced_property = preferred_view
85
+ self.ledger.update_conversation(conversation)
86
+
87
+ response_json = {
88
+ "model": self.settings.public_model_id,
89
+ "output_text": output_text,
90
+ "plan": plan.model_dump(),
91
+ "evidence": evidence,
92
+ "artifacts": artifacts,
93
+ }
94
+ response_record = self.ledger.create_response(
95
+ conversation.id,
96
+ request.previous_response_id,
97
+ request.model_dump(mode="json"),
98
+ response_json,
99
+ )
100
+ return {
101
+ "id": response_record.id,
102
+ "object": "response",
103
+ "created_at": response_record.created_at,
104
+ "status": "completed",
105
+ "model": self.settings.public_model_id,
106
+ "conversation_id": conversation.id,
107
+ "output": [
108
+ {
109
+ "id": f"msg_{response_record.id}",
110
+ "type": "message",
111
+ "role": "assistant",
112
+ "content": [{"type": "output_text", "text": output_text}],
113
+ }
114
+ ],
115
+ "output_text": output_text,
116
+ "artifacts": artifacts,
117
+ }
118
+
119
+ def _conversation_for_request(self, request: ResponsesRequest) -> ConversationRecord:
120
+ if request.previous_response_id:
121
+ previous = self.ledger.get_response(request.previous_response_id)
122
+ if previous:
123
+ conversation = self.ledger.get_conversation(previous.conversation_id)
124
+ if conversation:
125
+ return conversation
126
+ return self.ledger.create_conversation()
127
+
128
+ def _extract_text_and_images(self, input_value) -> tuple[str, list[dict]]:
129
+ if isinstance(input_value, str):
130
+ return input_value, []
131
+ texts: list[str] = []
132
+ images: list[dict] = []
133
+ for message in input_value:
134
+ content = message.get("content", []) if isinstance(message, dict) else message.content
135
+ if isinstance(content, str):
136
+ texts.append(content)
137
+ continue
138
+ for part in content:
139
+ if part.get("type") in {"input_text", "text"}:
140
+ texts.append(part.get("text", ""))
141
+ if part.get("type") == "input_image":
142
+ images.append(part)
143
+ if part.get("type") == "image_url":
144
+ image_url = part.get("image_url", {})
145
+ images.append({"type": "input_image", "image_url": image_url.get("url") if isinstance(image_url, dict) else image_url})
146
+ return "\n".join(t for t in texts if t), images
147
+
148
+ async def _ingest_image_ref(self, conversation: ConversationRecord, image_ref: dict) -> VisualAssetRecord:
149
+ source_file_id = image_ref.get("file_id")
150
+ if source_file_id:
151
+ file_record, data = self.get_file_bytes(source_file_id)
152
+ mime_type = file_record.mime_type
153
+ uri = file_record.uri
154
+ sha256 = file_record.sha256
155
+ else:
156
+ image_url = image_ref.get("image_url")
157
+ data, mime_type = await self._load_image_url(image_url)
158
+ uri, sha256 = self.store.put_bytes(data, suffix=self._suffix_for_mime(mime_type))
159
+ source_file_id = self.ledger.create_file("snapshotted-url-image", mime_type, len(data), uri, sha256).id
160
+ width, height = self._image_dimensions(data)
161
+ return self.ledger.create_asset(conversation.id, source_file_id, sha256, uri, mime_type, width, height)
162
+
163
+ async def _get_view(self, asset: VisualAssetRecord, view_type: str, parameters: dict) -> VisualViewRecord:
164
+ lens = self.lenses[view_type]
165
+ parameters_hash = canonical_hash(parameters)
166
+ cached = self.ledger.find_view(asset.sha256, lens.name, lens.version, parameters_hash)
167
+ if cached:
168
+ return cached
169
+ image_bytes = self.store.get_bytes(asset.original_uri)
170
+ view = await lens.run(asset, image_bytes, parameters)
171
+ return self.ledger.save_view(view)
172
+
173
+ async def _answer(self, intent: str, text: str, evidence: dict, views: list[VisualViewRecord]) -> tuple[str, list[dict]]:
174
+ artifacts: list[dict] = []
175
+ if intent == "answer_ocr":
176
+ for view in views:
177
+ if view.view_type == "ocr" and view.payload_json.get("lines"):
178
+ line = view.payload_json["lines"][0]
179
+ return f"{line['text']} (source bbox: {line['bbox']}, confidence: {line['confidence']})", artifacts
180
+ if intent == "query_chart":
181
+ for view in views:
182
+ if view.view_type == "chart_ir":
183
+ points = view.payload_json.get("series", [{}])[0].get("points", [])
184
+ match = next((p for p in points if str(p[0]).lower() == "q3"), None)
185
+ if match:
186
+ return f"Q3 value is {match[1]} (from ChartIR, confidence: {view.confidence}).", artifacts
187
+ if intent == "create_presentation":
188
+ artifacts.append({"type": "pptx", "status": "planned", "note": "PptxGenJS renderer service contract is available at /render"})
189
+ return "I prepared a presentation plan using the resolved visual theme. The PPTX renderer service can turn this into an editable deck.", artifacts
190
+ content = await self.llm.complete(
191
+ [
192
+ {"role": "developer", "content": "You are GLM-5.2 reasoning over compact visual evidence from the Visual Variable Runtime."},
193
+ {"role": "user", "content": json.dumps(evidence, ensure_ascii=False)},
194
+ ]
195
+ )
196
+ return content, artifacts
197
+
198
+ async def _load_image_url(self, image_url: str | None) -> tuple[bytes, str]:
199
+ if not image_url:
200
+ raise ValueError("input_image requires image_url or file_id")
201
+ if image_url.startswith("data:"):
202
+ header, encoded = image_url.split(",", 1)
203
+ mime_type = header.split(";")[0].removeprefix("data:")
204
+ data = base64.b64decode(encoded)
205
+ self._validate_image(data, mime_type)
206
+ return data, mime_type
207
+ self._validate_public_url(image_url)
208
+ async with httpx.AsyncClient(follow_redirects=False, timeout=20) as client:
209
+ response = await client.get(image_url)
210
+ for _ in range(3):
211
+ if response.status_code not in {301, 302, 303, 307, 308}:
212
+ break
213
+ location = response.headers.get("location")
214
+ self._validate_public_url(location)
215
+ response = await client.get(location)
216
+ response.raise_for_status()
217
+ mime_type = response.headers.get("content-type", "application/octet-stream").split(";")[0]
218
+ data = response.content
219
+ self._validate_image(data, mime_type)
220
+ return data, mime_type
221
+
222
+ def _validate_public_url(self, url: str | None) -> None:
223
+ if not url:
224
+ raise ValueError("empty URL")
225
+ parsed = urlparse(url)
226
+ if parsed.scheme not in {"http", "https"} or not parsed.hostname:
227
+ raise ValueError("unsupported image URL")
228
+ host = parsed.hostname.lower()
229
+ if host in {"localhost", "127.0.0.1", "::1"} or host.endswith(".local"):
230
+ raise ValueError("private image URL rejected")
231
+ for result in socket.getaddrinfo(host, None):
232
+ ip = ipaddress.ip_address(result[4][0])
233
+ if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
234
+ raise ValueError("private image URL rejected")
235
+
236
+ def _validate_image(self, data: bytes, mime_type: str) -> None:
237
+ if len(data) > self.settings.max_image_bytes:
238
+ raise ValueError("image exceeds max size")
239
+ if mime_type not in {"image/png", "image/jpeg", "image/webp"}:
240
+ raise ValueError("unsupported image MIME type")
241
+ width, height = self._image_dimensions(data)
242
+ if width * height > self.settings.max_image_pixels:
243
+ raise ValueError("image dimensions exceed limit")
244
+
245
+ def _image_dimensions(self, data: bytes) -> tuple[int, int]:
246
+ image = Image.open(io.BytesIO(data))
247
+ image.verify()
248
+ image = Image.open(io.BytesIO(data))
249
+ return image.size
250
+
251
+ def _suffix_for_mime(self, mime_type: str) -> str:
252
+ return {"image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp"}.get(mime_type, "")
apps/api/app/orchestration/visual_compiler.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from app.schemas.plans import VisualOperation, VisualPlan, VisualReference
4
+ from app.storage.models import VisualAssetRecord
5
+
6
+
7
+ def compile_visual_plan(intent: str, text: str, asset: VisualAssetRecord | None, views: list[str]) -> VisualPlan:
8
+ references = []
9
+ operations = []
10
+ if asset:
11
+ for idx, view in enumerate(views, start=1):
12
+ references.append(VisualReference(phrase=text[:80], asset_id=asset.id, view=view, confidence=0.86))
13
+ operations.append(VisualOperation(id=f"op_{idx}", op="get_view", asset_id=asset.id, view=view))
14
+ if intent == "create_presentation":
15
+ operations.append(VisualOperation(id=f"op_{len(operations)+1}", op="create_presentation", asset_id=asset.id if asset else None, parameters={"content_request": text}))
16
+ if not operations:
17
+ operations.append(VisualOperation(id="op_1", op="answer_from_evidence", parameters={"content_request": text}))
18
+ return VisualPlan(intent=intent, references=references, operations=operations)
apps/api/app/providers/llm/__pycache__/base.cpython-313.pyc ADDED
Binary file (874 Bytes). View file
 
apps/api/app/providers/llm/__pycache__/openai_compatible.cpython-313.pyc ADDED
Binary file (1.45 kB). View file
 
apps/api/app/providers/llm/__pycache__/zai_glm52.cpython-313.pyc ADDED
Binary file (2.28 kB). View file
 
apps/api/app/providers/llm/base.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+
7
+ class LlmProvider(ABC):
8
+ @abstractmethod
9
+ async def complete(self, messages: list[dict[str, Any]], response_format: dict[str, Any] | None = None) -> str:
10
+ raise NotImplementedError
apps/api/app/providers/llm/openai_compatible.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .base import LlmProvider
6
+
7
+
8
+ class FakeLlmProvider(LlmProvider):
9
+ async def complete(self, messages: list[dict[str, Any]], response_format: dict[str, Any] | None = None) -> str:
10
+ user_text = " ".join(str(m.get("content", "")) for m in messages if m.get("role") == "user")
11
+ if response_format:
12
+ return '{"intent":"answer_from_evidence","references":[],"operations":[]}'
13
+ return f"glm-5.2-visual-runtime received: {user_text}".strip()
apps/api/app/providers/llm/zai_glm52.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from app.config import Settings
8
+
9
+ from .base import LlmProvider
10
+
11
+
12
+ class ZaiGlm52Provider(LlmProvider):
13
+ def __init__(self, settings: Settings):
14
+ self.settings = settings
15
+
16
+ async def complete(self, messages: list[dict[str, Any]], response_format: dict[str, Any] | None = None) -> str:
17
+ if not self.settings.glm_api_key:
18
+ raise RuntimeError("GLM_API_KEY is required outside test mode")
19
+ payload: dict[str, Any] = {
20
+ "model": self.settings.glm_model,
21
+ "messages": messages,
22
+ }
23
+ if response_format:
24
+ payload["response_format"] = response_format
25
+ async with httpx.AsyncClient(timeout=60) as client:
26
+ response = await client.post(
27
+ f"{self.settings.glm_base_url.rstrip('/')}/chat/completions",
28
+ headers={"Authorization": f"Bearer {self.settings.glm_api_key}"},
29
+ json=payload,
30
+ )
31
+ response.raise_for_status()
32
+ data = response.json()
33
+ return data["choices"][0]["message"]["content"]
apps/api/app/providers/ocr/__pycache__/base.cpython-313.pyc ADDED
Binary file (836 Bytes). View file
 
apps/api/app/providers/ocr/__pycache__/fake.cpython-313.pyc ADDED
Binary file (1.05 kB). View file
 
apps/api/app/providers/ocr/__pycache__/glm_ocr.cpython-313.pyc ADDED
Binary file (602 Bytes). View file
 
apps/api/app/providers/ocr/__pycache__/local.cpython-313.pyc ADDED
Binary file (2.09 kB). View file
 
apps/api/app/providers/ocr/base.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class OcrProvider(ABC):
7
+ @abstractmethod
8
+ async def extract(self, image_bytes: bytes, region: tuple[float, float, float, float] | None = None) -> dict:
9
+ raise NotImplementedError