longparser 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- longparser/__init__.py +104 -0
- longparser/chunkers/__init__.py +5 -0
- longparser/chunkers/hybrid_chunker.py +1046 -0
- longparser/extractors/__init__.py +9 -0
- longparser/extractors/base.py +62 -0
- longparser/extractors/docling_extractor.py +2065 -0
- longparser/extractors/latex_ocr.py +404 -0
- longparser/integrations/__init__.py +31 -0
- longparser/integrations/langchain.py +138 -0
- longparser/integrations/llamaindex.py +157 -0
- longparser/pipeline/__init__.py +8 -0
- longparser/pipeline/orchestrator.py +230 -0
- longparser/py.typed +0 -0
- longparser/schemas.py +247 -0
- longparser/server/__init__.py +22 -0
- longparser/server/app.py +1045 -0
- longparser/server/chat/__init__.py +39 -0
- longparser/server/chat/callbacks.py +110 -0
- longparser/server/chat/engine.py +341 -0
- longparser/server/chat/graph.py +176 -0
- longparser/server/chat/llm_chain.py +153 -0
- longparser/server/chat/retriever.py +111 -0
- longparser/server/chat/schemas.py +164 -0
- longparser/server/db.py +656 -0
- longparser/server/embeddings.py +181 -0
- longparser/server/queue.py +97 -0
- longparser/server/routers/__init__.py +0 -0
- longparser/server/schemas.py +204 -0
- longparser/server/vectorstores.py +443 -0
- longparser/server/worker.py +480 -0
- longparser/utils/__init__.py +5 -0
- longparser/utils/rtl_detector.py +93 -0
- longparser-0.1.0.dist-info/METADATA +337 -0
- longparser-0.1.0.dist-info/RECORD +36 -0
- longparser-0.1.0.dist-info/WHEEL +5 -0
- longparser-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Embedding engine for LongParser — wraps LangChain providers.
|
|
2
|
+
|
|
3
|
+
Supported: HuggingFace (local), OpenAI, Gemini.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Global lock for single-flight dimension discovery within a process
|
|
18
|
+
_dim_lock = threading.Lock()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EmbeddingEngine:
|
|
22
|
+
"""Multi-provider embedding engine.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
provider:
|
|
27
|
+
"huggingface", "openai", or "gemini".
|
|
28
|
+
model_name:
|
|
29
|
+
Model ID (e.g., "BAAI/bge-base-en-v1.5", "text-embedding-3-small").
|
|
30
|
+
dimensions:
|
|
31
|
+
Optional override for embedding dimensions (useful for OpenAI/Gemini
|
|
32
|
+
to avoid API calls or specifically configure text-embedding-3).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
provider: str = "huggingface",
|
|
38
|
+
model_name: str = "BAAI/bge-base-en-v1.5",
|
|
39
|
+
dimensions: Optional[int] = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.provider = provider.lower()
|
|
42
|
+
self.model_name = model_name
|
|
43
|
+
self.configured_dimensions = dimensions
|
|
44
|
+
self._dim: Optional[int] = dimensions
|
|
45
|
+
|
|
46
|
+
# Provider-specific configurations
|
|
47
|
+
self._gemini_doc_task = "RETRIEVAL_DOCUMENT"
|
|
48
|
+
self._gemini_query_task = "RETRIEVAL_QUERY"
|
|
49
|
+
self._hf_normalize = True
|
|
50
|
+
|
|
51
|
+
if self.provider == "openai":
|
|
52
|
+
from langchain_openai import OpenAIEmbeddings
|
|
53
|
+
# Pass dimensions kwargs if provided (supported for text-embedding-3*)
|
|
54
|
+
kwargs = {}
|
|
55
|
+
if self.configured_dimensions:
|
|
56
|
+
kwargs["dimensions"] = self.configured_dimensions
|
|
57
|
+
self.model = OpenAIEmbeddings(model=self.model_name, **kwargs)
|
|
58
|
+
logger.info(f"EmbeddingEngine: Initialized OpenAI {model_name}")
|
|
59
|
+
|
|
60
|
+
elif self.provider == "gemini":
|
|
61
|
+
from langchain_google_genai import GoogleGenerativeAIEmbeddings
|
|
62
|
+
kwargs = {}
|
|
63
|
+
if self.configured_dimensions:
|
|
64
|
+
kwargs["output_dimensionality"] = self.configured_dimensions
|
|
65
|
+
self.model = GoogleGenerativeAIEmbeddings(model=self.model_name, **kwargs)
|
|
66
|
+
logger.info(f"EmbeddingEngine: Initialized Gemini {model_name}")
|
|
67
|
+
|
|
68
|
+
elif self.provider == "huggingface":
|
|
69
|
+
from langchain_huggingface import HuggingFaceEmbeddings
|
|
70
|
+
self.model = HuggingFaceEmbeddings(
|
|
71
|
+
model_name=self.model_name,
|
|
72
|
+
encode_kwargs={"normalize_embeddings": self._hf_normalize}
|
|
73
|
+
)
|
|
74
|
+
logger.info(f"EmbeddingEngine: Initialized HuggingFace {model_name}")
|
|
75
|
+
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(f"Unknown embedding provider: {provider}")
|
|
78
|
+
|
|
79
|
+
def get_fingerprint(self) -> str:
|
|
80
|
+
"""Return a stable 10-char SHA1 hash of the full embedding configuration.
|
|
81
|
+
This is used to isolate vector spaces when models/configs change.
|
|
82
|
+
"""
|
|
83
|
+
config = {
|
|
84
|
+
"p": self.provider,
|
|
85
|
+
"m": self.model_name,
|
|
86
|
+
"d": self.configured_dimensions,
|
|
87
|
+
}
|
|
88
|
+
if self.provider == "gemini":
|
|
89
|
+
config["t_doc"] = self._gemini_doc_task
|
|
90
|
+
config["t_qry"] = self._gemini_query_task
|
|
91
|
+
elif self.provider == "huggingface":
|
|
92
|
+
config["n"] = self._hf_normalize
|
|
93
|
+
|
|
94
|
+
# Stable json dump
|
|
95
|
+
cfg_str = json.dumps(config, sort_keys=True)
|
|
96
|
+
return hashlib.sha1(cfg_str.encode("utf-8")).hexdigest()[:10]
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def dim(self) -> int:
|
|
100
|
+
"""Lazy-evaluated, thread-safe, cross-process cached embedding dimension."""
|
|
101
|
+
if self._dim is not None:
|
|
102
|
+
return self._dim
|
|
103
|
+
|
|
104
|
+
# Global dimension lock
|
|
105
|
+
with _dim_lock:
|
|
106
|
+
# Check if another thread resolved it while waiting
|
|
107
|
+
if self._dim is not None:
|
|
108
|
+
return self._dim
|
|
109
|
+
|
|
110
|
+
fp = self.get_fingerprint()
|
|
111
|
+
cache_key = f"cleanrag:embed_dim:{fp}"
|
|
112
|
+
|
|
113
|
+
# 1) Try Redis cross-process cache if available
|
|
114
|
+
try:
|
|
115
|
+
import redis
|
|
116
|
+
redis_url = os.getenv("LONGPARSER_REDIS_URL", "redis://localhost:6379/0")
|
|
117
|
+
r = redis.from_url(redis_url, socket_connect_timeout=1)
|
|
118
|
+
r.ping() # Fail fast if unavailable
|
|
119
|
+
cached = r.get(cache_key)
|
|
120
|
+
if cached:
|
|
121
|
+
self._dim = int(cached)
|
|
122
|
+
logger.debug(f"Resolved dim from Redis cache: {self._dim}")
|
|
123
|
+
return self._dim
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.debug(f"Redis cache check failed (likely no redis): {e}")
|
|
126
|
+
|
|
127
|
+
# 2) Fallback: perform an API call to discover it.
|
|
128
|
+
# We strictly use the Document retrieval task setting to ensure dimension matches index.
|
|
129
|
+
logger.info(f"Discovering embedding dimension for {self.provider}/{self.model_name}...")
|
|
130
|
+
test_doc = ["test"]
|
|
131
|
+
try:
|
|
132
|
+
if self.provider == "gemini":
|
|
133
|
+
# Explicitly pass the document task type
|
|
134
|
+
test_vec = self.model.embed_documents(test_doc, task_type=self._gemini_doc_task)[0]
|
|
135
|
+
else:
|
|
136
|
+
test_vec = self.model.embed_documents(test_doc)[0]
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f"Failed to discover embedding dimension: {e}")
|
|
139
|
+
raise # Fail loudly! Missing API keys should crash here.
|
|
140
|
+
|
|
141
|
+
self._dim = len(test_vec)
|
|
142
|
+
logger.info(f"Discovered dimension: {self._dim}")
|
|
143
|
+
|
|
144
|
+
# 3) Cache it in Redis
|
|
145
|
+
try:
|
|
146
|
+
if 'r' in locals():
|
|
147
|
+
r.set(cache_key, self._dim)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
return self._dim
|
|
152
|
+
|
|
153
|
+
def embed_chunks(self, texts: list[str], batch_size: int = 64) -> list[list[float]]:
|
|
154
|
+
"""Embed a list of text chunks.
|
|
155
|
+
|
|
156
|
+
Returns list of float vectors (one per chunk).
|
|
157
|
+
"""
|
|
158
|
+
if not texts:
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
if self.provider == "gemini":
|
|
162
|
+
# Gemini strictly enforces maximum 100 texts per batch request
|
|
163
|
+
effective_batch = min(batch_size, 100)
|
|
164
|
+
all_embeddings = []
|
|
165
|
+
for i in range(0, len(texts), effective_batch):
|
|
166
|
+
batch = texts[i:i + effective_batch]
|
|
167
|
+
# Pass explicit task_type parameter per call
|
|
168
|
+
all_embeddings.extend(
|
|
169
|
+
self.model.embed_documents(batch, task_type=self._gemini_doc_task)
|
|
170
|
+
)
|
|
171
|
+
return all_embeddings
|
|
172
|
+
else:
|
|
173
|
+
return self.model.embed_documents(texts)
|
|
174
|
+
|
|
175
|
+
def embed_query(self, query: str) -> list[float]:
|
|
176
|
+
"""Embed a single search query."""
|
|
177
|
+
if self.provider == "gemini":
|
|
178
|
+
# For queries, Gemini optimizes with a different task_type
|
|
179
|
+
return self.model.embed_query(query, task_type=self._gemini_query_task)
|
|
180
|
+
else:
|
|
181
|
+
return self.model.embed_query(query)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Abstracted job queue backend for LongParser.
|
|
2
|
+
|
|
3
|
+
ARQ (async Redis queue) is the default implementation.
|
|
4
|
+
The QueueBackend ABC allows swapping to Celery/Dramatiq
|
|
5
|
+
without rewriting business logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QueueBackend(ABC):
|
|
18
|
+
"""Abstract queue interface — swap implementations without changing routes."""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
async def enqueue(self, task_name: str, payload: dict) -> str:
|
|
22
|
+
"""Enqueue a task. Returns task/job reference ID."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def cancel(self, task_id: str) -> bool:
|
|
27
|
+
"""Cancel a queued/running task. Returns True if cancelled."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def status(self, task_id: str) -> dict:
|
|
32
|
+
"""Get task status and progress."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ARQBackend(QueueBackend):
|
|
37
|
+
"""ARQ (async Redis queue) implementation."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, redis_url: str = "redis://localhost:6379"):
|
|
40
|
+
self.redis_url = redis_url
|
|
41
|
+
self._pool = None
|
|
42
|
+
|
|
43
|
+
async def _get_pool(self):
|
|
44
|
+
if self._pool is None:
|
|
45
|
+
from arq import create_pool
|
|
46
|
+
from arq.connections import RedisSettings
|
|
47
|
+
|
|
48
|
+
url = self.redis_url.replace("redis://", "")
|
|
49
|
+
# Strip database number (e.g., /0) if present
|
|
50
|
+
url = url.split("/")[0]
|
|
51
|
+
host, _, port_str = url.partition(":")
|
|
52
|
+
port = int(port_str) if port_str else 6379
|
|
53
|
+
self._pool = await create_pool(RedisSettings(host=host, port=port))
|
|
54
|
+
return self._pool
|
|
55
|
+
|
|
56
|
+
async def enqueue(self, task_name: str, payload: dict) -> str:
|
|
57
|
+
"""Enqueue a task via ARQ."""
|
|
58
|
+
pool = await self._get_pool()
|
|
59
|
+
job = await pool.enqueue_job(task_name, **payload)
|
|
60
|
+
job_id = job.job_id if job else "unknown"
|
|
61
|
+
logger.info(f"Enqueued {task_name} → ARQ job {job_id}")
|
|
62
|
+
return job_id
|
|
63
|
+
|
|
64
|
+
async def cancel(self, task_id: str) -> bool:
|
|
65
|
+
"""Cancel via ARQ job abort (best-effort)."""
|
|
66
|
+
try:
|
|
67
|
+
from arq.jobs import Job
|
|
68
|
+
pool = await self._get_pool()
|
|
69
|
+
job = Job(task_id, pool)
|
|
70
|
+
await job.abort()
|
|
71
|
+
return True
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.warning(f"Failed to abort ARQ job {task_id}: {e}")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
async def status(self, task_id: str) -> dict:
|
|
77
|
+
"""Get ARQ job status."""
|
|
78
|
+
try:
|
|
79
|
+
from arq.jobs import Job
|
|
80
|
+
pool = await self._get_pool()
|
|
81
|
+
job = Job(task_id, pool)
|
|
82
|
+
info = await job.info()
|
|
83
|
+
if info is None:
|
|
84
|
+
return {"status": "unknown"}
|
|
85
|
+
return {
|
|
86
|
+
"status": info.status,
|
|
87
|
+
"result": info.result,
|
|
88
|
+
"enqueue_time": str(info.enqueue_time) if info.enqueue_time else None,
|
|
89
|
+
}
|
|
90
|
+
except Exception:
|
|
91
|
+
return {"status": "unknown"}
|
|
92
|
+
|
|
93
|
+
async def close(self) -> None:
|
|
94
|
+
"""Close the Redis pool."""
|
|
95
|
+
if self._pool:
|
|
96
|
+
await self._pool.close()
|
|
97
|
+
self._pool = None
|
|
File without changes
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""API request/response models for LongParser HITL review system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from ..schemas import BlockType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Enums
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
class JobStatus(str, Enum):
|
|
21
|
+
"""Job lifecycle states."""
|
|
22
|
+
QUEUED = "queued"
|
|
23
|
+
EXTRACTING = "extracting"
|
|
24
|
+
READY_FOR_REVIEW = "ready_for_review"
|
|
25
|
+
FINALIZING = "finalizing"
|
|
26
|
+
FINALIZED = "finalized"
|
|
27
|
+
EMBEDDING = "embedding"
|
|
28
|
+
INDEXED = "indexed"
|
|
29
|
+
FAILED = "failed"
|
|
30
|
+
CANCELLED = "cancelled"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ReviewStatus(str, Enum):
|
|
34
|
+
"""Block/chunk review states."""
|
|
35
|
+
PENDING = "pending"
|
|
36
|
+
APPROVED = "approved"
|
|
37
|
+
EDITED = "edited"
|
|
38
|
+
REJECTED = "rejected"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FinalizePolicy(str, Enum):
|
|
42
|
+
"""What to do with pending items on finalize."""
|
|
43
|
+
REJECT_PENDING = "reject_pending"
|
|
44
|
+
APPROVE_PENDING = "approve_pending"
|
|
45
|
+
REQUIRE_ALL_APPROVED = "require_all_approved"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UserRole(str, Enum):
|
|
49
|
+
"""RBAC roles."""
|
|
50
|
+
ADMIN = "admin"
|
|
51
|
+
REVIEWER = "reviewer"
|
|
52
|
+
VIEWER = "viewer"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Revision (append-only audit trail)
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
class Revision(BaseModel):
|
|
60
|
+
"""Immutable record of a block/chunk edit."""
|
|
61
|
+
revision_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
62
|
+
entity_type: str # "block" | "chunk"
|
|
63
|
+
entity_id: str
|
|
64
|
+
previous_revision_id: Optional[str] = None
|
|
65
|
+
action: ReviewStatus
|
|
66
|
+
original_text: str
|
|
67
|
+
edited_text: Optional[str] = None
|
|
68
|
+
edited_type: Optional[BlockType] = None
|
|
69
|
+
reviewer_id: str = ""
|
|
70
|
+
reviewer_note: str = ""
|
|
71
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# API Request Models
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
class BlockReviewUpdate(BaseModel):
|
|
79
|
+
"""Request body for PATCH /jobs/{id}/blocks/{bid}."""
|
|
80
|
+
status: ReviewStatus
|
|
81
|
+
edited_text: Optional[str] = None
|
|
82
|
+
edited_type: Optional[BlockType] = None
|
|
83
|
+
reviewer_note: str = ""
|
|
84
|
+
version: int # optimistic locking
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ChunkReviewUpdate(BaseModel):
|
|
88
|
+
"""Request body for PATCH /jobs/{id}/chunks/{cid}."""
|
|
89
|
+
status: ReviewStatus
|
|
90
|
+
edited_text: Optional[str] = None
|
|
91
|
+
reviewer_note: str = ""
|
|
92
|
+
version: int # optimistic locking
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class FinalizeRequest(BaseModel):
|
|
96
|
+
"""Request body for POST /jobs/{id}/finalize."""
|
|
97
|
+
finalize_policy: FinalizePolicy = FinalizePolicy.REJECT_PENDING
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class EmbedRequest(BaseModel):
|
|
101
|
+
"""Request body for POST /jobs/{id}/embed."""
|
|
102
|
+
provider: str = Field(
|
|
103
|
+
default_factory=lambda: os.getenv("LONGPARSER_EMBED_PROVIDER", "huggingface")
|
|
104
|
+
)
|
|
105
|
+
model: str = Field(
|
|
106
|
+
default_factory=lambda: os.getenv("LONGPARSER_EMBED_MODEL", "BAAI/bge-base-en-v1.5")
|
|
107
|
+
)
|
|
108
|
+
vector_db: str = Field(
|
|
109
|
+
default_factory=lambda: os.getenv("LONGPARSER_VECTOR_DB", "chroma")
|
|
110
|
+
) # "chroma" | "faiss" | "qdrant"
|
|
111
|
+
collection_name: Optional[str] = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SearchRequest(BaseModel):
|
|
115
|
+
"""Request body for POST /search."""
|
|
116
|
+
query: str
|
|
117
|
+
job_id: str
|
|
118
|
+
top_k: int = 5
|
|
119
|
+
index_version: Optional[str] = None # defaults to latest
|
|
120
|
+
filters: dict = Field(default_factory=dict)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# API Response Models
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
class ReviewProgress(BaseModel):
|
|
128
|
+
"""Review completion stats."""
|
|
129
|
+
approved: int = 0
|
|
130
|
+
edited: int = 0
|
|
131
|
+
rejected: int = 0
|
|
132
|
+
pending: int = 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class JobResponse(BaseModel):
|
|
136
|
+
"""Response body for GET /jobs/{id}."""
|
|
137
|
+
job_id: str
|
|
138
|
+
tenant_id: str
|
|
139
|
+
status: JobStatus
|
|
140
|
+
source_file: str
|
|
141
|
+
file_hash: str = ""
|
|
142
|
+
total_pages: int = 0
|
|
143
|
+
total_blocks: int = 0
|
|
144
|
+
total_chunks: int = 0
|
|
145
|
+
review_progress: ReviewProgress = Field(default_factory=ReviewProgress)
|
|
146
|
+
created_at: datetime
|
|
147
|
+
finalized_at: Optional[datetime] = None
|
|
148
|
+
error: Optional[str] = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class JobListResponse(BaseModel):
|
|
152
|
+
"""Response body for GET /jobs."""
|
|
153
|
+
jobs: list[JobResponse]
|
|
154
|
+
total: int
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class BlockResponse(BaseModel):
|
|
158
|
+
"""Block data for API responses (confidence excluded)."""
|
|
159
|
+
block_id: str
|
|
160
|
+
type: BlockType
|
|
161
|
+
text: str
|
|
162
|
+
order_index: int = 0
|
|
163
|
+
heading_level: Optional[int] = None
|
|
164
|
+
indent_level: int = 0
|
|
165
|
+
hierarchy_path: list[str] = Field(default_factory=list)
|
|
166
|
+
page_number: int = 0
|
|
167
|
+
review_status: ReviewStatus = ReviewStatus.PENDING
|
|
168
|
+
current_revision_id: Optional[str] = None
|
|
169
|
+
version: int = 1
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ChunkResponse(BaseModel):
|
|
173
|
+
"""Chunk data for API responses."""
|
|
174
|
+
chunk_id: str
|
|
175
|
+
text: str
|
|
176
|
+
token_count: int = 0
|
|
177
|
+
chunk_type: str = ""
|
|
178
|
+
section_path: list[str] = Field(default_factory=list)
|
|
179
|
+
page_numbers: list[int] = Field(default_factory=list)
|
|
180
|
+
block_ids: list[str] = Field(default_factory=list)
|
|
181
|
+
review_status: ReviewStatus = ReviewStatus.PENDING
|
|
182
|
+
current_revision_id: Optional[str] = None
|
|
183
|
+
version: int = 1
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class SearchResult(BaseModel):
|
|
187
|
+
"""Single search result."""
|
|
188
|
+
chunk_id: str
|
|
189
|
+
text: str
|
|
190
|
+
score: float
|
|
191
|
+
chunk_type: str = ""
|
|
192
|
+
section_path: list[str] = Field(default_factory=list)
|
|
193
|
+
page_numbers: list[int] = Field(default_factory=list)
|
|
194
|
+
block_ids: list[str] = Field(default_factory=list)
|
|
195
|
+
metadata: dict = Field(default_factory=dict)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class SearchResponse(BaseModel):
|
|
199
|
+
"""Response body for POST /search."""
|
|
200
|
+
results: list[SearchResult]
|
|
201
|
+
index_version: str
|
|
202
|
+
model: str
|
|
203
|
+
query: str
|
|
204
|
+
total: int
|