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,153 @@
|
|
|
1
|
+
"""LangChain LLM abstraction for LongParser Chat.
|
|
2
|
+
|
|
3
|
+
Replaces custom llm_router.py with LangChain's provider-specific chat models.
|
|
4
|
+
Supports: OpenAI, Gemini, Groq, OpenRouter.
|
|
5
|
+
Includes: with_structured_output, with_retry.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from .schemas import ChatConfig
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Default models per provider (updated Feb 2026)
|
|
20
|
+
DEFAULT_MODELS: dict[str, str] = {
|
|
21
|
+
"openai": "gpt-5.3-codex",
|
|
22
|
+
"gemini": "gemini-2.5-flash",
|
|
23
|
+
"groq": "openai/gpt-oss-120b",
|
|
24
|
+
"openrouter": "openai/gpt-5.3-codex",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _create_openai(model: str, temperature: float, max_tokens: int,
|
|
29
|
+
max_retries: int, callbacks: Optional[list] = None):
|
|
30
|
+
"""Create OpenAI chat model."""
|
|
31
|
+
from langchain_openai import ChatOpenAI
|
|
32
|
+
return ChatOpenAI(
|
|
33
|
+
model=model,
|
|
34
|
+
temperature=temperature,
|
|
35
|
+
max_tokens=max_tokens,
|
|
36
|
+
max_retries=max_retries,
|
|
37
|
+
callbacks=callbacks or [],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _create_gemini(model: str, temperature: float, max_tokens: int,
|
|
42
|
+
max_retries: int, callbacks: Optional[list] = None):
|
|
43
|
+
"""Create Google Gemini chat model."""
|
|
44
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
45
|
+
return ChatGoogleGenerativeAI(
|
|
46
|
+
model=model,
|
|
47
|
+
temperature=temperature,
|
|
48
|
+
max_output_tokens=max_tokens,
|
|
49
|
+
max_retries=max_retries,
|
|
50
|
+
callbacks=callbacks or [],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _create_groq(model: str, temperature: float, max_tokens: int,
|
|
55
|
+
max_retries: int, callbacks: Optional[list] = None):
|
|
56
|
+
"""Create Groq chat model."""
|
|
57
|
+
from langchain_groq import ChatGroq
|
|
58
|
+
return ChatGroq(
|
|
59
|
+
model=model,
|
|
60
|
+
temperature=temperature,
|
|
61
|
+
max_tokens=max_tokens,
|
|
62
|
+
max_retries=max_retries,
|
|
63
|
+
callbacks=callbacks or [],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _create_openrouter(model: str, temperature: float, max_tokens: int,
|
|
68
|
+
max_retries: int, callbacks: Optional[list] = None):
|
|
69
|
+
"""Create OpenRouter chat model (OpenAI-compatible)."""
|
|
70
|
+
from langchain_openai import ChatOpenAI
|
|
71
|
+
return ChatOpenAI(
|
|
72
|
+
model=model,
|
|
73
|
+
temperature=temperature,
|
|
74
|
+
max_tokens=max_tokens,
|
|
75
|
+
max_retries=max_retries,
|
|
76
|
+
base_url="https://openrouter.ai/api/v1",
|
|
77
|
+
api_key=os.getenv("OPENROUTER_API_KEY", ""),
|
|
78
|
+
callbacks=callbacks or [],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_CREATORS = {
|
|
83
|
+
"openai": _create_openai,
|
|
84
|
+
"gemini": _create_gemini,
|
|
85
|
+
"groq": _create_groq,
|
|
86
|
+
"openrouter": _create_openrouter,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_chat_model(
|
|
91
|
+
provider: Optional[str] = None,
|
|
92
|
+
model: Optional[str] = None,
|
|
93
|
+
config: Optional[ChatConfig] = None,
|
|
94
|
+
*,
|
|
95
|
+
temperature: float = 0.1,
|
|
96
|
+
max_tokens: Optional[int] = None,
|
|
97
|
+
json_mode: bool = False,
|
|
98
|
+
callbacks: Optional[list] = None,
|
|
99
|
+
):
|
|
100
|
+
"""Create a LangChain chat model for any supported provider.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
provider: LLM provider name (openai, gemini, groq, openrouter).
|
|
104
|
+
model: Model name. If None, uses config or provider default.
|
|
105
|
+
config: ChatConfig for defaults and reliability settings.
|
|
106
|
+
temperature: Sampling temperature.
|
|
107
|
+
max_tokens: Max output tokens.
|
|
108
|
+
json_mode: If True, wraps with .with_structured_output(LLMAnswer).
|
|
109
|
+
callbacks: Optional LangChain callback handlers.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
A LangChain BaseChatModel (or structured output wrapper).
|
|
113
|
+
"""
|
|
114
|
+
config = config or ChatConfig()
|
|
115
|
+
provider = provider or config.llm_provider
|
|
116
|
+
model = model or config.llm_model or DEFAULT_MODELS.get(provider, "gpt-4o")
|
|
117
|
+
max_tokens = max_tokens or config.max_output_tokens
|
|
118
|
+
|
|
119
|
+
creator = _CREATORS.get(provider)
|
|
120
|
+
if not creator:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Unknown LLM provider: {provider}. "
|
|
123
|
+
f"Supported: {', '.join(_CREATORS)}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
llm = creator(
|
|
127
|
+
model=model,
|
|
128
|
+
temperature=temperature,
|
|
129
|
+
max_tokens=max_tokens,
|
|
130
|
+
max_retries=config.llm_max_retries,
|
|
131
|
+
callbacks=callbacks,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Structured output: returns Pydantic LLMAnswer directly
|
|
135
|
+
if json_mode:
|
|
136
|
+
from .schemas import LLMAnswer
|
|
137
|
+
llm = llm.with_structured_output(LLMAnswer)
|
|
138
|
+
|
|
139
|
+
return llm
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_plain_chat_model(
|
|
143
|
+
provider: Optional[str] = None,
|
|
144
|
+
model: Optional[str] = None,
|
|
145
|
+
config: Optional[ChatConfig] = None,
|
|
146
|
+
):
|
|
147
|
+
"""Get a plain (non-structured) chat model for summarization / plain text tasks."""
|
|
148
|
+
return get_chat_model(
|
|
149
|
+
provider=provider,
|
|
150
|
+
model=model,
|
|
151
|
+
config=config,
|
|
152
|
+
json_mode=False,
|
|
153
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""LangChain retriever for LongParser Chat.
|
|
2
|
+
|
|
3
|
+
Wraps existing vector store + embeddings as a LangChain BaseRetriever,
|
|
4
|
+
enabling plugging into LCEL chains.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from langchain_core.callbacks import CallbackManagerForRetrieverRun
|
|
13
|
+
from langchain_core.documents import Document
|
|
14
|
+
from langchain_core.retrievers import BaseRetriever
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LongParserRetriever(BaseRetriever):
|
|
21
|
+
"""LangChain retriever backed by LongParser's existing vector store infra.
|
|
22
|
+
|
|
23
|
+
Connects to the same Chroma/FAISS/Qdrant indexes built by the embed pipeline.
|
|
24
|
+
Uses LangChain-native embeddings for query encoding.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
db: Any = Field(exclude=True)
|
|
28
|
+
tenant_id: str
|
|
29
|
+
job_id: str
|
|
30
|
+
top_k: int = 5
|
|
31
|
+
|
|
32
|
+
# Resolved at runtime from index_version
|
|
33
|
+
_vector_db: Optional[str] = None
|
|
34
|
+
_model_name: Optional[str] = None
|
|
35
|
+
_provider: Optional[str] = None
|
|
36
|
+
_configured_dimensions: Optional[int] = None
|
|
37
|
+
_collection: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
class Config:
|
|
40
|
+
arbitrary_types_allowed = True
|
|
41
|
+
|
|
42
|
+
async def _resolve_index(self) -> None:
|
|
43
|
+
"""Load index metadata from MongoDB (lazy, once)."""
|
|
44
|
+
if self._model_name is not None:
|
|
45
|
+
return
|
|
46
|
+
iv_doc = await self.db.get_latest_index_version(self.tenant_id, self.job_id)
|
|
47
|
+
if not iv_doc:
|
|
48
|
+
raise ValueError(f"No embedding index for job {self.job_id}")
|
|
49
|
+
self._vector_db = iv_doc.get("vector_db", "chroma")
|
|
50
|
+
self._model_name = iv_doc["model"]
|
|
51
|
+
self._provider = iv_doc.get("provider", "huggingface")
|
|
52
|
+
self._configured_dimensions = iv_doc.get("configured_dimensions")
|
|
53
|
+
self._collection = iv_doc.get("collection", "longparser")
|
|
54
|
+
|
|
55
|
+
def _get_relevant_documents(
|
|
56
|
+
self,
|
|
57
|
+
query: str,
|
|
58
|
+
*,
|
|
59
|
+
run_manager: Optional[CallbackManagerForRetrieverRun] = None,
|
|
60
|
+
) -> list[Document]:
|
|
61
|
+
"""Sync retrieval — delegates to existing vector store."""
|
|
62
|
+
import asyncio
|
|
63
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
64
|
+
self._aget_relevant_documents(query, run_manager=run_manager)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def _aget_relevant_documents(
|
|
68
|
+
self,
|
|
69
|
+
query: str,
|
|
70
|
+
*,
|
|
71
|
+
run_manager: Optional[CallbackManagerForRetrieverRun] = None,
|
|
72
|
+
) -> list[Document]:
|
|
73
|
+
"""Async retrieval using existing EmbeddingEngine + vector store."""
|
|
74
|
+
await self._resolve_index()
|
|
75
|
+
|
|
76
|
+
from ..embeddings import EmbeddingEngine
|
|
77
|
+
from ..vectorstores import get_vector_store
|
|
78
|
+
|
|
79
|
+
# Embed query using same model that built the index
|
|
80
|
+
engine = EmbeddingEngine(
|
|
81
|
+
provider=self._provider,
|
|
82
|
+
model_name=self._model_name,
|
|
83
|
+
dimensions=self._configured_dimensions
|
|
84
|
+
)
|
|
85
|
+
query_embedding = engine.embed_query(query)
|
|
86
|
+
|
|
87
|
+
# Search vector DB
|
|
88
|
+
store = get_vector_store(
|
|
89
|
+
self._vector_db,
|
|
90
|
+
collection_name=self._collection,
|
|
91
|
+
index_fingerprint=engine.get_fingerprint(),
|
|
92
|
+
)
|
|
93
|
+
filters = {"tenant_id": self.tenant_id, "job_id": self.job_id}
|
|
94
|
+
results = store.search(query_embedding, top_k=self.top_k, filters=filters)
|
|
95
|
+
|
|
96
|
+
# Convert to LangChain Documents
|
|
97
|
+
documents = []
|
|
98
|
+
for r in results:
|
|
99
|
+
meta = r.get("metadata", {})
|
|
100
|
+
documents.append(Document(
|
|
101
|
+
page_content=r.get("document", ""),
|
|
102
|
+
metadata={
|
|
103
|
+
"chunk_id": meta.get("chunk_id", r.get("id", "")),
|
|
104
|
+
"score": r.get("score", 0),
|
|
105
|
+
"chunk_type": meta.get("chunk_type", ""),
|
|
106
|
+
"page_numbers": meta.get("page_numbers", []),
|
|
107
|
+
"block_ids": meta.get("block_ids", []),
|
|
108
|
+
},
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
return documents
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Pydantic models for LongParser Chat API."""
|
|
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
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Enums
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
class FactSourceType(str, Enum):
|
|
19
|
+
"""Allowed fact source types."""
|
|
20
|
+
DOC = "doc"
|
|
21
|
+
USER = "user"
|
|
22
|
+
ASSISTANT_INFERENCE = "assistant_inference" # ephemeral — never persisted
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Config (read from env with defaults)
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
class ChatConfig(BaseModel):
|
|
30
|
+
"""Chat configuration — all values from env with sensible defaults."""
|
|
31
|
+
|
|
32
|
+
llm_provider: str = Field(
|
|
33
|
+
default_factory=lambda: os.getenv("LONGPARSER_LLM_PROVIDER", "openai")
|
|
34
|
+
)
|
|
35
|
+
llm_model: str = Field(
|
|
36
|
+
default_factory=lambda: os.getenv("LONGPARSER_LLM_MODEL", "gpt-4o")
|
|
37
|
+
)
|
|
38
|
+
max_input_tokens: int = Field(
|
|
39
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_CHAT_MAX_INPUT_TOKENS", "1000"))
|
|
40
|
+
)
|
|
41
|
+
max_output_tokens: int = Field(
|
|
42
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_CHAT_MAX_OUTPUT_TOKENS", "2000"))
|
|
43
|
+
)
|
|
44
|
+
max_prompt_tokens: int = Field(
|
|
45
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_CHAT_MAX_PROMPT_TOKENS", "6000"))
|
|
46
|
+
)
|
|
47
|
+
max_top_k: int = Field(
|
|
48
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_CHAT_MAX_TOP_K", "10"))
|
|
49
|
+
)
|
|
50
|
+
rate_limit: int = Field(
|
|
51
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_CHAT_RATE_LIMIT", "20"))
|
|
52
|
+
)
|
|
53
|
+
short_term_turns: int = Field(
|
|
54
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_CHAT_SHORT_TERM_TURNS", "8"))
|
|
55
|
+
)
|
|
56
|
+
summarize_every: int = Field(
|
|
57
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_CHAT_SUMMARIZE_EVERY", "10"))
|
|
58
|
+
)
|
|
59
|
+
extract_facts_every: int = Field(
|
|
60
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_CHAT_EXTRACT_FACTS_EVERY", "20"))
|
|
61
|
+
)
|
|
62
|
+
max_facts: int = Field(
|
|
63
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_CHAT_MAX_FACTS", "20"))
|
|
64
|
+
)
|
|
65
|
+
llm_timeout: float = Field(
|
|
66
|
+
default_factory=lambda: float(os.getenv("LONGPARSER_LLM_TIMEOUT", "30"))
|
|
67
|
+
)
|
|
68
|
+
llm_max_retries: int = Field(
|
|
69
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_LLM_MAX_RETRIES", "3"))
|
|
70
|
+
)
|
|
71
|
+
ttl_days: int = Field(
|
|
72
|
+
default_factory=lambda: int(os.getenv("LONGPARSER_CHAT_TTL_DAYS", "30"))
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Request / Response Models
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
class CreateSessionRequest(BaseModel):
|
|
81
|
+
"""POST /chat/sessions — create a chat session."""
|
|
82
|
+
job_id: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ChatRequest(BaseModel):
|
|
86
|
+
"""POST /chat — ask a question."""
|
|
87
|
+
session_id: str
|
|
88
|
+
job_id: str
|
|
89
|
+
question: str
|
|
90
|
+
llm_provider: Optional[str] = None # override env default
|
|
91
|
+
llm_model: Optional[str] = None # override env default
|
|
92
|
+
top_k: int = 5
|
|
93
|
+
idempotency_key: Optional[str] = None
|
|
94
|
+
require_approval: bool = False # opt-in HITL review
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class HITLResumeRequest(BaseModel):
|
|
98
|
+
"""POST /chat/resume — resume a paused HITL chat."""
|
|
99
|
+
session_id: str
|
|
100
|
+
thread_id: str # LangGraph thread ID
|
|
101
|
+
action: str # "approve" | "edit" | "reject"
|
|
102
|
+
edited_answer: Optional[str] = None # only for action="edit"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class SourceRef(BaseModel):
|
|
106
|
+
"""A reference to a retrieved chunk used as evidence."""
|
|
107
|
+
chunk_id: str
|
|
108
|
+
score: float
|
|
109
|
+
text: str = ""
|
|
110
|
+
page_numbers: list[int] = Field(default_factory=list)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ChatResponse(BaseModel):
|
|
114
|
+
"""Response body for POST /chat."""
|
|
115
|
+
session_id: str
|
|
116
|
+
turn_id: str
|
|
117
|
+
answer: str
|
|
118
|
+
sources: list[SourceRef] = Field(default_factory=list)
|
|
119
|
+
status: str = "complete" # "complete" | "pending_review"
|
|
120
|
+
thread_id: Optional[str] = None # set when status="pending_review"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class LLMAnswer(BaseModel):
|
|
124
|
+
"""Structured LLM output — enforced via with_structured_output."""
|
|
125
|
+
answer: str
|
|
126
|
+
cited_chunk_ids: list[str] = Field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Turn & Fact Models (stored in MongoDB)
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
class Turn(BaseModel):
|
|
134
|
+
"""A single Q&A turn in a chat session."""
|
|
135
|
+
turn_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
136
|
+
question: str
|
|
137
|
+
answer: str
|
|
138
|
+
sources: list[SourceRef] = Field(default_factory=list)
|
|
139
|
+
archived: bool = False
|
|
140
|
+
idempotency_key: Optional[str] = None
|
|
141
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Fact(BaseModel):
|
|
145
|
+
"""A long-term fact extracted from conversation."""
|
|
146
|
+
type: str # entities_from_doc | user_preferences | decisions
|
|
147
|
+
source: FactSourceType
|
|
148
|
+
fact: str
|
|
149
|
+
supporting_chunk_ids: list[str] = Field(default_factory=list)
|
|
150
|
+
confidence: float = 0.0
|
|
151
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class SessionInfo(BaseModel):
|
|
155
|
+
"""Response for GET /chat/sessions/{id}."""
|
|
156
|
+
session_id: str
|
|
157
|
+
tenant_id: str
|
|
158
|
+
job_id: str
|
|
159
|
+
turn_count: int = 0
|
|
160
|
+
rolling_summary: str = ""
|
|
161
|
+
long_term_facts: list[Fact] = Field(default_factory=list)
|
|
162
|
+
created_at: datetime
|
|
163
|
+
updated_at: Optional[datetime] = None
|
|
164
|
+
deleted_at: Optional[datetime] = None
|