control plane and api issues
This commit is contained in:
parent
f7cf33c610
commit
943f6e7844
160 changed files with 3068 additions and 929 deletions
|
|
@ -64,14 +64,21 @@ FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Node.js, curl, and uv
|
# Install Node.js, curl, uv, and pg0 dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
|
libxml2 \
|
||||||
|
libssl3 \
|
||||||
|
libgssapi-krb5-2 \
|
||||||
|
&& apt-get install -y libicu72 || apt-get install -y libicu74 || apt-get install -y libicu* \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
&& apt-get install -y nodejs \
|
&& apt-get install -y nodejs \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& pip install --no-cache-dir uv
|
&& pip install --no-cache-dir uv
|
||||||
|
|
||||||
|
# Create non-root user (PostgreSQL cannot run as root)
|
||||||
|
RUN useradd -m -s /bin/bash hindsight
|
||||||
|
|
||||||
# Copy API with virtual environment from builder
|
# Copy API with virtual environment from builder
|
||||||
COPY --from=api-base /app/api /app/api
|
COPY --from=api-base /app/api /app/api
|
||||||
|
|
||||||
|
|
@ -96,13 +103,19 @@ WORKDIR /app
|
||||||
COPY docker/standalone/start-all.sh /app/start-all.sh
|
COPY docker/standalone/start-all.sh /app/start-all.sh
|
||||||
RUN chmod +x /app/start-all.sh
|
RUN chmod +x /app/start-all.sh
|
||||||
|
|
||||||
# Create data directory for pg0
|
# Create data directory for pg0 and set ownership
|
||||||
RUN mkdir -p /app/data
|
RUN mkdir -p /app/data && chown -R hindsight:hindsight /app
|
||||||
|
|
||||||
# Install pg0 to /root/.hindsight/bin/pg0
|
# Switch to non-root user
|
||||||
RUN mkdir -p /root/.hindsight/bin /root/.local/bin && \
|
USER hindsight
|
||||||
export PATH="/root/.hindsight/bin:/root/.local/bin:$PATH" && \
|
|
||||||
curl -fsSL https://raw.githubusercontent.com/vectorize-io/pg0/main/install.sh | bash
|
# Install pg0
|
||||||
|
RUN curl -fsSL https://raw.githubusercontent.com/vectorize-io/pg0/main/install.sh | bash
|
||||||
|
|
||||||
|
# Start pg0 once to verify it works and pre-download PostgreSQL libraries
|
||||||
|
RUN pg0 --help && \
|
||||||
|
pg0 start --wait && \
|
||||||
|
pg0 stop
|
||||||
|
|
||||||
# Expose ports
|
# Expose ports
|
||||||
EXPOSE 8888 3000
|
EXPOSE 8888 3000
|
||||||
|
|
@ -113,7 +126,7 @@ ENV HINDSIGHT_API_PORT=8888
|
||||||
ENV HINDSIGHT_API_LOG_LEVEL=info
|
ENV HINDSIGHT_API_LOG_LEVEL=info
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
|
ENV HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
|
||||||
ENV PATH="/app/api/.venv/bin:$PATH"
|
ENV PATH="/home/hindsight/.local/bin:/app/api/.venv/bin:${PATH}"
|
||||||
|
|
||||||
# Run startup script
|
# Run startup script
|
||||||
CMD ["/app/start-all.sh"]
|
CMD ["/app/start-all.sh"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""add_retain_params_to_documents
|
||||||
|
|
||||||
|
Revision ID: c8e5f2a3b4d1
|
||||||
|
Revises: b7c4d8e9f1a2
|
||||||
|
Create Date: 2025-12-02 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'c8e5f2a3b4d1'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = 'b7c4d8e9f1a2'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Add retain_params JSONB column to documents table."""
|
||||||
|
|
||||||
|
# Add retain_params column to store parameters passed during retain
|
||||||
|
op.add_column('documents', sa.Column('retain_params', postgresql.JSONB(), nullable=True))
|
||||||
|
|
||||||
|
# Add index for efficient queries on retain_params
|
||||||
|
op.create_index('idx_documents_retain_params', 'documents', ['retain_params'], postgresql_using='gin')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Remove retain_params column from documents table."""
|
||||||
|
|
||||||
|
# Drop index
|
||||||
|
op.drop_index('idx_documents_retain_params', table_name='documents')
|
||||||
|
|
||||||
|
# Drop column
|
||||||
|
op.drop_column('documents', 'retain_params')
|
||||||
|
|
@ -84,7 +84,7 @@ class RecallRequest(BaseModel):
|
||||||
model_config = ConfigDict(json_schema_extra={
|
model_config = ConfigDict(json_schema_extra={
|
||||||
"example": {
|
"example": {
|
||||||
"query": "What did Alice say about machine learning?",
|
"query": "What did Alice say about machine learning?",
|
||||||
"types": ["world", "agent"],
|
"types": ["world", "bank"],
|
||||||
"budget": "mid",
|
"budget": "mid",
|
||||||
"max_tokens": 4096,
|
"max_tokens": 4096,
|
||||||
"trace": True,
|
"trace": True,
|
||||||
|
|
@ -279,7 +279,8 @@ class MemoryItem(BaseModel):
|
||||||
"content": "Alice mentioned she's working on a new ML model",
|
"content": "Alice mentioned she's working on a new ML model",
|
||||||
"timestamp": "2024-01-15T10:30:00Z",
|
"timestamp": "2024-01-15T10:30:00Z",
|
||||||
"context": "team meeting",
|
"context": "team meeting",
|
||||||
"metadata": {"source": "slack", "channel": "engineering"}
|
"metadata": {"source": "slack", "channel": "engineering"},
|
||||||
|
"document_id": "meeting_notes_2024_01_15"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -287,6 +288,10 @@ class MemoryItem(BaseModel):
|
||||||
timestamp: Optional[datetime] = None
|
timestamp: Optional[datetime] = None
|
||||||
context: Optional[str] = None
|
context: Optional[str] = None
|
||||||
metadata: Optional[Dict[str, str]] = None
|
metadata: Optional[Dict[str, str]] = None
|
||||||
|
document_id: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Optional document ID for this memory item."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RetainRequest(BaseModel):
|
class RetainRequest(BaseModel):
|
||||||
|
|
@ -296,20 +301,20 @@ class RetainRequest(BaseModel):
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"content": "Alice works at Google",
|
"content": "Alice works at Google",
|
||||||
"context": "work"
|
"context": "work",
|
||||||
|
"document_id": "conversation_123"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"content": "Bob went hiking yesterday",
|
"content": "Bob went hiking yesterday",
|
||||||
"timestamp": "2024-01-15T10:00:00Z"
|
"timestamp": "2024-01-15T10:00:00Z",
|
||||||
|
"document_id": "conversation_123"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"document_id": "conversation_123",
|
|
||||||
"async": False
|
"async": False
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
items: List[MemoryItem]
|
items: List[MemoryItem]
|
||||||
document_id: Optional[str] = None
|
|
||||||
async_: bool = Field(
|
async_: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
alias="async",
|
alias="async",
|
||||||
|
|
@ -325,7 +330,6 @@ class RetainResponse(BaseModel):
|
||||||
"example": {
|
"example": {
|
||||||
"success": True,
|
"success": True,
|
||||||
"bank_id": "user123",
|
"bank_id": "user123",
|
||||||
"document_id": "conversation_123",
|
|
||||||
"items_count": 2,
|
"items_count": 2,
|
||||||
"async": False
|
"async": False
|
||||||
}
|
}
|
||||||
|
|
@ -334,7 +338,6 @@ class RetainResponse(BaseModel):
|
||||||
|
|
||||||
success: bool
|
success: bool
|
||||||
bank_id: str
|
bank_id: str
|
||||||
document_id: Optional[str] = None
|
|
||||||
items_count: int
|
items_count: int
|
||||||
async_: bool = Field(alias="async", serialization_alias="async", description="Whether the operation was processed asynchronously")
|
async_: bool = Field(alias="async", serialization_alias="async", description="Whether the operation was processed asynchronously")
|
||||||
|
|
||||||
|
|
@ -414,7 +417,7 @@ class ReflectResponse(BaseModel):
|
||||||
{
|
{
|
||||||
"id": "456",
|
"id": "456",
|
||||||
"text": "I discussed AI applications last week",
|
"text": "I discussed AI applications last week",
|
||||||
"type": "agent"
|
"type": "bank"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -680,6 +683,27 @@ class DocumentResponse(BaseModel):
|
||||||
memory_unit_count: int
|
memory_unit_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class ChunkResponse(BaseModel):
|
||||||
|
"""Response model for get chunk endpoint."""
|
||||||
|
model_config = ConfigDict(json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"chunk_id": "user123_session_1_0",
|
||||||
|
"document_id": "session_1",
|
||||||
|
"bank_id": "user123",
|
||||||
|
"chunk_index": 0,
|
||||||
|
"chunk_text": "This is the first chunk of the document...",
|
||||||
|
"created_at": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
chunk_id: str
|
||||||
|
document_id: str
|
||||||
|
bank_id: str
|
||||||
|
chunk_index: int
|
||||||
|
chunk_text: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
|
||||||
class DeleteResponse(BaseModel):
|
class DeleteResponse(BaseModel):
|
||||||
"""Response model for delete operations."""
|
"""Response model for delete operations."""
|
||||||
model_config = ConfigDict(json_schema_extra={
|
model_config = ConfigDict(json_schema_extra={
|
||||||
|
|
@ -859,9 +883,8 @@ def _register_routes(app: FastAPI):
|
||||||
|
|
||||||
The type parameter is optional and must be one of:
|
The type parameter is optional and must be one of:
|
||||||
- 'world': General knowledge about people, places, events, and things that happen
|
- 'world': General knowledge about people, places, events, and things that happen
|
||||||
- 'agent': Memories about what the AI agent did, actions taken, and tasks performed
|
- 'bank': Memories about what the AI agent did, actions taken, and tasks performed
|
||||||
- 'opinion': The bank's formed beliefs, perspectives, and viewpoints
|
- 'opinion': The bank's formed beliefs, perspectives, and viewpoints
|
||||||
- 'observation': Synthesized observations about entities (generated automatically)
|
|
||||||
|
|
||||||
Set include_entities=true to get entity observations alongside recall results.
|
Set include_entities=true to get entity observations alongside recall results.
|
||||||
""",
|
""",
|
||||||
|
|
@ -873,10 +896,10 @@ def _register_routes(app: FastAPI):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Validate types
|
# Validate types
|
||||||
valid_fact_types = ["world", "agent", "opinion", "observation"]
|
valid_fact_types = ["world", "bank", "opinion"]
|
||||||
|
|
||||||
# Default to world, agent, opinion if not specified (exclude observation by default)
|
# Default to world, agent, opinion if not specified (exclude observation by default)
|
||||||
fact_types = request.types if request.types else ["world", "agent", "opinion"]
|
fact_types = request.types if request.types else ["world", "bank", "opinion"]
|
||||||
for ft in fact_types:
|
for ft in fact_types:
|
||||||
if ft not in valid_fact_types:
|
if ft not in valid_fact_types:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -1367,6 +1390,34 @@ def _register_routes(app: FastAPI):
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/v1/default/chunks/{chunk_id}",
|
||||||
|
response_model=ChunkResponse,
|
||||||
|
summary="Get chunk details",
|
||||||
|
description="Get a specific chunk by its ID",
|
||||||
|
operation_id="get_chunk"
|
||||||
|
)
|
||||||
|
async def api_get_chunk(chunk_id: str):
|
||||||
|
"""
|
||||||
|
Get a specific chunk with its text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chunk_id: Chunk ID (from path, format: bank_id_document_id_chunk_index)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
chunk = await app.state.memory.get_chunk(chunk_id)
|
||||||
|
if not chunk:
|
||||||
|
raise HTTPException(status_code=404, detail="Chunk not found")
|
||||||
|
return chunk
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||||
|
logger.error(f"Error in /v1/default/chunks/{chunk_id}: {error_detail}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.delete(
|
@app.delete(
|
||||||
"/v1/default/banks/{bank_id}/documents/{document_id}",
|
"/v1/default/banks/{bank_id}/documents/{document_id}",
|
||||||
summary="Delete a document",
|
summary="Delete a document",
|
||||||
|
|
@ -1517,10 +1568,12 @@ This operation cannot be undone.
|
||||||
"""Get memory bank profile (personality + background)."""
|
"""Get memory bank profile (personality + background)."""
|
||||||
try:
|
try:
|
||||||
profile = await app.state.memory.get_bank_profile(bank_id)
|
profile = await app.state.memory.get_bank_profile(bank_id)
|
||||||
|
# Convert PersonalityTraits object to dict for Pydantic
|
||||||
|
personality_dict = profile["personality"].model_dump() if hasattr(profile["personality"], 'model_dump') else dict(profile["personality"])
|
||||||
return BankProfileResponse(
|
return BankProfileResponse(
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
name=profile["name"],
|
name=profile["name"],
|
||||||
personality=profile["personality"], # Already a PersonalityTraits object
|
personality=PersonalityTraits(**personality_dict),
|
||||||
background=profile["background"]
|
background=profile["background"]
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1550,10 +1603,11 @@ This operation cannot be undone.
|
||||||
|
|
||||||
# Get updated profile
|
# Get updated profile
|
||||||
profile = await app.state.memory.get_bank_profile(bank_id)
|
profile = await app.state.memory.get_bank_profile(bank_id)
|
||||||
|
personality_dict = profile["personality"].model_dump() if hasattr(profile["personality"], 'model_dump') else dict(profile["personality"])
|
||||||
return BankProfileResponse(
|
return BankProfileResponse(
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
name=profile["name"],
|
name=profile["name"],
|
||||||
personality=profile["personality"], # Already a PersonalityTraits object
|
personality=PersonalityTraits(**personality_dict),
|
||||||
background=profile["background"]
|
background=profile["background"]
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1638,7 +1692,7 @@ This operation cannot be undone.
|
||||||
async with acquire_with_retry(pool) as conn:
|
async with acquire_with_retry(pool) as conn:
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE agents
|
UPDATE banks
|
||||||
SET background = $2,
|
SET background = $2,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE bank_id = $1
|
WHERE bank_id = $1
|
||||||
|
|
@ -1650,10 +1704,11 @@ This operation cannot be undone.
|
||||||
|
|
||||||
# Get final profile
|
# Get final profile
|
||||||
final_profile = await app.state.memory.get_bank_profile(bank_id)
|
final_profile = await app.state.memory.get_bank_profile(bank_id)
|
||||||
|
personality_dict = final_profile["personality"].model_dump() if hasattr(final_profile["personality"], 'model_dump') else dict(final_profile["personality"])
|
||||||
return BankProfileResponse(
|
return BankProfileResponse(
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
name=final_profile["name"],
|
name=final_profile["name"],
|
||||||
personality=final_profile["personality"], # Already a PersonalityTraits object
|
personality=PersonalityTraits(**personality_dict),
|
||||||
background=final_profile["background"]
|
background=final_profile["background"]
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1677,7 +1732,7 @@ This operation cannot be undone.
|
||||||
- Efficient batch processing
|
- Efficient batch processing
|
||||||
- Automatic fact extraction from natural language
|
- Automatic fact extraction from natural language
|
||||||
- Entity recognition and linking
|
- Entity recognition and linking
|
||||||
- Document tracking with automatic upsert (when document_id is provided)
|
- Document tracking with automatic upsert (when document_id is provided on items)
|
||||||
- Temporal and semantic linking
|
- Temporal and semantic linking
|
||||||
- Optional asynchronous processing
|
- Optional asynchronous processing
|
||||||
|
|
||||||
|
|
@ -1697,7 +1752,7 @@ This operation cannot be undone.
|
||||||
- Waits for processing to complete
|
- Waits for processing to complete
|
||||||
- Returns after all memories are stored
|
- Returns after all memories are stored
|
||||||
|
|
||||||
Note: If document_id is provided and already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior).
|
Note: If a memory item has a document_id that already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior). Items with the same document_id are grouped together for efficient processing.
|
||||||
""",
|
""",
|
||||||
operation_id="retain_memories"
|
operation_id="retain_memories"
|
||||||
)
|
)
|
||||||
|
|
@ -1716,6 +1771,8 @@ This operation cannot be undone.
|
||||||
content_dict["context"] = item.context
|
content_dict["context"] = item.context
|
||||||
if item.metadata:
|
if item.metadata:
|
||||||
content_dict["metadata"] = item.metadata
|
content_dict["metadata"] = item.metadata
|
||||||
|
if item.document_id:
|
||||||
|
content_dict["document_id"] = item.document_id
|
||||||
contents.append(content_dict)
|
contents.append(content_dict)
|
||||||
|
|
||||||
if request.async_:
|
if request.async_:
|
||||||
|
|
@ -1727,14 +1784,13 @@ This operation cannot be undone.
|
||||||
async with acquire_with_retry(pool) as conn:
|
async with acquire_with_retry(pool) as conn:
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO async_operations (id, bank_id, task_type, items_count, document_id)
|
INSERT INTO async_operations (id, bank_id, task_type, items_count)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4)
|
||||||
""",
|
""",
|
||||||
operation_id,
|
operation_id,
|
||||||
bank_id,
|
bank_id,
|
||||||
'retain',
|
'retain',
|
||||||
len(contents),
|
len(contents)
|
||||||
request.document_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Submit task to background queue
|
# Submit task to background queue
|
||||||
|
|
@ -1742,8 +1798,7 @@ This operation cannot be undone.
|
||||||
'type': 'batch_put',
|
'type': 'batch_put',
|
||||||
'operation_id': str(operation_id),
|
'operation_id': str(operation_id),
|
||||||
'bank_id': bank_id,
|
'bank_id': bank_id,
|
||||||
'contents': contents,
|
'contents': contents
|
||||||
'document_id': request.document_id
|
|
||||||
})
|
})
|
||||||
|
|
||||||
logging.info(f"Retain task queued for bank_id={bank_id}, {len(contents)} items, operation_id={operation_id}")
|
logging.info(f"Retain task queued for bank_id={bank_id}, {len(contents)} items, operation_id={operation_id}")
|
||||||
|
|
@ -1751,7 +1806,6 @@ This operation cannot be undone.
|
||||||
return RetainResponse(
|
return RetainResponse(
|
||||||
success=True,
|
success=True,
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
document_id=request.document_id,
|
|
||||||
items_count=len(contents),
|
items_count=len(contents),
|
||||||
async_=True
|
async_=True
|
||||||
)
|
)
|
||||||
|
|
@ -1760,14 +1814,12 @@ This operation cannot be undone.
|
||||||
with metrics.record_operation("retain", bank_id=bank_id):
|
with metrics.record_operation("retain", bank_id=bank_id):
|
||||||
result = await app.state.memory.retain_batch_async(
|
result = await app.state.memory.retain_batch_async(
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
contents=contents,
|
contents=contents
|
||||||
document_id=request.document_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return RetainResponse(
|
return RetainResponse(
|
||||||
success=True,
|
success=True,
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
document_id=request.document_id,
|
|
||||||
items_count=len(contents),
|
items_count=len(contents),
|
||||||
async_=False
|
async_=False
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ logger = logging.getLogger(__name__)
|
||||||
# Disable httpx logging
|
# Disable httpx logging
|
||||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Global semaphore to limit concurrent LLM requests across all instances
|
||||||
|
_global_llm_semaphore = asyncio.Semaphore(32)
|
||||||
|
|
||||||
|
|
||||||
class OutputTooLongError(Exception):
|
class OutputTooLongError(Exception):
|
||||||
"""
|
"""
|
||||||
|
|
@ -69,12 +72,13 @@ class LLMConfig:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create client (private - use .call() method instead)
|
# Create client (private - use .call() method instead)
|
||||||
|
# Disable automatic retries - we handle retries in the call() method
|
||||||
if self.provider == "ollama":
|
if self.provider == "ollama":
|
||||||
self._client = AsyncOpenAI(api_key="ollama", base_url=self.base_url)
|
self._client = AsyncOpenAI(api_key="ollama", base_url=self.base_url, max_retries=0)
|
||||||
elif self.base_url:
|
elif self.base_url:
|
||||||
self._client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
|
self._client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url, max_retries=0)
|
||||||
else:
|
else:
|
||||||
self._client = AsyncOpenAI(api_key=self.api_key)
|
self._client = AsyncOpenAI(api_key=self.api_key, max_retries=0)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Initialized LLM: provider={self.provider}, model={self.model}, base_url={self.base_url}"
|
f"Initialized LLM: provider={self.provider}, model={self.model}, base_url={self.base_url}"
|
||||||
|
|
@ -109,6 +113,8 @@ class LLMConfig:
|
||||||
Raises:
|
Raises:
|
||||||
Exception: Re-raises any API errors after all retries are exhausted
|
Exception: Re-raises any API errors after all retries are exhausted
|
||||||
"""
|
"""
|
||||||
|
# Use global semaphore to limit concurrent requests
|
||||||
|
async with _global_llm_semaphore:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
call_params = {
|
call_params = {
|
||||||
|
|
@ -192,13 +198,12 @@ class LLMConfig:
|
||||||
jitter = backoff * 0.2 * (2 * (time.time() % 1) - 1)
|
jitter = backoff * 0.2 * (2 * (time.time() % 1) - 1)
|
||||||
sleep_time = backoff + jitter
|
sleep_time = backoff + jitter
|
||||||
|
|
||||||
logger.warning(
|
# Only log if it's a non-retryable error or final attempt
|
||||||
f"LLM error on attempt {attempt + 1}/{max_retries + 1}. "
|
# Silent retry for common transient errors like capacity exceeded
|
||||||
f"Retrying in {sleep_time:.2f}s... Error: {str(e)}"
|
|
||||||
)
|
|
||||||
await asyncio.sleep(sleep_time)
|
await asyncio.sleep(sleep_time)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Non-retryable API error after {max_retries + 1} attempts: {str(e)}")
|
# Log only on final failed attempt
|
||||||
|
logger.error(f"API error after {max_retries + 1} attempts: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ This implements a sophisticated memory architecture that combines:
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
from typing import Any, Dict, List, Optional, Tuple, Union, TypedDict
|
||||||
import asyncpg
|
import asyncpg
|
||||||
import asyncio
|
import asyncio
|
||||||
from .embeddings import Embeddings, SentenceTransformersEmbeddings
|
from .embeddings import Embeddings, SentenceTransformersEmbeddings
|
||||||
|
|
@ -22,6 +22,23 @@ import uuid
|
||||||
import logging
|
import logging
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class RetainContentDict(TypedDict, total=False):
|
||||||
|
"""Type definition for content items in retain_batch_async.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
content: Text content to store (required)
|
||||||
|
context: Context about the content (optional)
|
||||||
|
event_date: When the content occurred (optional, defaults to now)
|
||||||
|
metadata: Custom key-value metadata (optional)
|
||||||
|
document_id: Document ID for this content item (optional)
|
||||||
|
"""
|
||||||
|
content: str # Required
|
||||||
|
context: str
|
||||||
|
event_date: datetime
|
||||||
|
metadata: Dict[str, str]
|
||||||
|
document_id: str
|
||||||
|
|
||||||
from .query_analyzer import QueryAnalyzer
|
from .query_analyzer import QueryAnalyzer
|
||||||
from .search.scoring import (
|
from .search.scoring import (
|
||||||
calculate_recency_weight,
|
calculate_recency_weight,
|
||||||
|
|
@ -218,19 +235,17 @@ class MemoryEngine:
|
||||||
Handler for batch retain tasks.
|
Handler for batch retain tasks.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task_dict: Dict with 'bank_id', 'contents', 'document_id'
|
task_dict: Dict with 'bank_id', 'contents'
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
bank_id = task_dict.get('bank_id')
|
bank_id = task_dict.get('bank_id')
|
||||||
contents = task_dict.get('contents', [])
|
contents = task_dict.get('contents', [])
|
||||||
document_id = task_dict.get('document_id')
|
|
||||||
|
|
||||||
logger.info(f"[BATCH_RETAIN_TASK] Starting background batch retain for bank_id={bank_id}, {len(contents)} items")
|
logger.info(f"[BATCH_RETAIN_TASK] Starting background batch retain for bank_id={bank_id}, {len(contents)} items")
|
||||||
|
|
||||||
await self.retain_batch_async(
|
await self.retain_batch_async(
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
contents=contents,
|
contents=contents
|
||||||
document_id=document_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"[BATCH_RETAIN_TASK] Completed background batch retain for bank_id={bank_id}")
|
logger.info(f"[BATCH_RETAIN_TASK] Completed background batch retain for bank_id={bank_id}")
|
||||||
|
|
@ -604,15 +619,19 @@ class MemoryEngine:
|
||||||
Returns:
|
Returns:
|
||||||
List of created unit IDs
|
List of created unit IDs
|
||||||
"""
|
"""
|
||||||
# Use retain_batch_async with a single item (avoids code duplication)
|
# Build content dict
|
||||||
result = await self.retain_batch_async(
|
content_dict: RetainContentDict = {
|
||||||
bank_id=bank_id,
|
|
||||||
contents=[{
|
|
||||||
"content": content,
|
"content": content,
|
||||||
"context": context,
|
"context": context,
|
||||||
"event_date": event_date
|
"event_date": event_date
|
||||||
}],
|
}
|
||||||
document_id=document_id,
|
if document_id:
|
||||||
|
content_dict["document_id"] = document_id
|
||||||
|
|
||||||
|
# Use retain_batch_async with a single item (avoids code duplication)
|
||||||
|
result = await self.retain_batch_async(
|
||||||
|
bank_id=bank_id,
|
||||||
|
contents=[content_dict],
|
||||||
fact_type_override=fact_type_override,
|
fact_type_override=fact_type_override,
|
||||||
confidence_score=confidence_score
|
confidence_score=confidence_score
|
||||||
)
|
)
|
||||||
|
|
@ -623,7 +642,7 @@ class MemoryEngine:
|
||||||
async def retain_batch_async(
|
async def retain_batch_async(
|
||||||
self,
|
self,
|
||||||
bank_id: str,
|
bank_id: str,
|
||||||
contents: List[Dict[str, Any]],
|
contents: List[RetainContentDict],
|
||||||
document_id: Optional[str] = None,
|
document_id: Optional[str] = None,
|
||||||
fact_type_override: Optional[str] = None,
|
fact_type_override: Optional[str] = None,
|
||||||
confidence_score: Optional[float] = None,
|
confidence_score: Optional[float] = None,
|
||||||
|
|
@ -643,19 +662,32 @@ class MemoryEngine:
|
||||||
- "content" (required): Text content to store
|
- "content" (required): Text content to store
|
||||||
- "context" (optional): Context about the memory
|
- "context" (optional): Context about the memory
|
||||||
- "event_date" (optional): When the event occurred
|
- "event_date" (optional): When the event occurred
|
||||||
document_id: Optional document ID for tracking (always upserts if document already exists)
|
- "document_id" (optional): Document ID for this specific content item
|
||||||
|
document_id: **DEPRECATED** - Use "document_id" key in each content dict instead.
|
||||||
|
Applies the same document_id to ALL content items that don't specify their own.
|
||||||
fact_type_override: Override fact type for all facts ('world', 'bank', 'opinion')
|
fact_type_override: Override fact type for all facts ('world', 'bank', 'opinion')
|
||||||
confidence_score: Confidence score for opinions (0.0 to 1.0)
|
confidence_score: Confidence score for opinions (0.0 to 1.0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of lists of unit IDs (one list per content item)
|
List of lists of unit IDs (one list per content item)
|
||||||
|
|
||||||
Example:
|
Example (new style - per-content document_id):
|
||||||
unit_ids = await memory.retain_batch_async(
|
unit_ids = await memory.retain_batch_async(
|
||||||
bank_id="user123",
|
bank_id="user123",
|
||||||
contents=[
|
contents=[
|
||||||
{"content": "Alice works at Google", "context": "conversation"},
|
{"content": "Alice works at Google", "document_id": "doc1"},
|
||||||
{"content": "Bob loves Python", "context": "conversation"},
|
{"content": "Bob loves Python", "document_id": "doc2"},
|
||||||
|
{"content": "More about Alice", "document_id": "doc1"},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# Returns: [["unit-id-1"], ["unit-id-2"], ["unit-id-3"]]
|
||||||
|
|
||||||
|
Example (deprecated style - batch-level document_id):
|
||||||
|
unit_ids = await memory.retain_batch_async(
|
||||||
|
bank_id="user123",
|
||||||
|
contents=[
|
||||||
|
{"content": "Alice works at Google"},
|
||||||
|
{"content": "Bob loves Python"},
|
||||||
],
|
],
|
||||||
document_id="meeting-2024-01-15"
|
document_id="meeting-2024-01-15"
|
||||||
)
|
)
|
||||||
|
|
@ -666,11 +698,17 @@ class MemoryEngine:
|
||||||
if not contents:
|
if not contents:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Apply batch-level document_id to contents that don't have their own (backwards compatibility)
|
||||||
|
if document_id:
|
||||||
|
for item in contents:
|
||||||
|
if "document_id" not in item:
|
||||||
|
item["document_id"] = document_id
|
||||||
|
|
||||||
# Auto-chunk large batches by character count to avoid timeouts and memory issues
|
# Auto-chunk large batches by character count to avoid timeouts and memory issues
|
||||||
# Calculate total character count
|
# Calculate total character count
|
||||||
total_chars = sum(len(item.get("content", "")) for item in contents)
|
total_chars = sum(len(item.get("content", "")) for item in contents)
|
||||||
|
|
||||||
CHARS_PER_BATCH = 500_000
|
CHARS_PER_BATCH = 600_000
|
||||||
|
|
||||||
if total_chars > CHARS_PER_BATCH:
|
if total_chars > CHARS_PER_BATCH:
|
||||||
# Split into smaller batches based on character count
|
# Split into smaller batches based on character count
|
||||||
|
|
@ -732,7 +770,7 @@ class MemoryEngine:
|
||||||
async def _retain_batch_async_internal(
|
async def _retain_batch_async_internal(
|
||||||
self,
|
self,
|
||||||
bank_id: str,
|
bank_id: str,
|
||||||
contents: List[Dict[str, Any]],
|
contents: List[RetainContentDict],
|
||||||
document_id: Optional[str] = None,
|
document_id: Optional[str] = None,
|
||||||
is_first_batch: bool = True,
|
is_first_batch: bool = True,
|
||||||
fact_type_override: Optional[str] = None,
|
fact_type_override: Optional[str] = None,
|
||||||
|
|
@ -768,6 +806,7 @@ class MemoryEngine:
|
||||||
task_backend=self._task_backend,
|
task_backend=self._task_backend,
|
||||||
format_date_fn=self._format_readable_date,
|
format_date_fn=self._format_readable_date,
|
||||||
duplicate_checker_fn=self._find_duplicate_facts_batch,
|
duplicate_checker_fn=self._find_duplicate_facts_batch,
|
||||||
|
regenerate_observations_fn=self._regenerate_observations_sync,
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
contents_dicts=contents,
|
contents_dicts=contents,
|
||||||
document_id=document_id,
|
document_id=document_id,
|
||||||
|
|
@ -982,7 +1021,11 @@ class MemoryEngine:
|
||||||
temporal_results = []
|
temporal_results = []
|
||||||
aggregated_timings = {"semantic": 0.0, "bm25": 0.0, "graph": 0.0, "temporal": 0.0}
|
aggregated_timings = {"semantic": 0.0, "bm25": 0.0, "graph": 0.0, "temporal": 0.0}
|
||||||
|
|
||||||
for ft_semantic, ft_bm25, ft_graph, ft_temporal, ft_timings in all_retrievals:
|
for idx, (ft_semantic, ft_bm25, ft_graph, ft_temporal, ft_timings) in enumerate(all_retrievals):
|
||||||
|
# Log fact types in this retrieval batch
|
||||||
|
ft_name = fact_type[idx] if idx < len(fact_type) else "unknown"
|
||||||
|
logger.debug(f"[SEARCH {search_id}] Fact type '{ft_name}': semantic={len(ft_semantic)}, bm25={len(ft_bm25)}, graph={len(ft_graph)}, temporal={len(ft_temporal) if ft_temporal else 0}")
|
||||||
|
|
||||||
semantic_results.extend(ft_semantic)
|
semantic_results.extend(ft_semantic)
|
||||||
bm25_results.extend(ft_bm25)
|
bm25_results.extend(ft_bm25)
|
||||||
graph_results.extend(ft_graph)
|
graph_results.extend(ft_graph)
|
||||||
|
|
@ -996,6 +1039,14 @@ class MemoryEngine:
|
||||||
if not temporal_results:
|
if not temporal_results:
|
||||||
temporal_results = None
|
temporal_results = None
|
||||||
|
|
||||||
|
# Sort combined results by score (descending) so higher-scored results
|
||||||
|
# get better ranks in the trace, regardless of fact type
|
||||||
|
semantic_results.sort(key=lambda r: r.similarity if hasattr(r, 'similarity') else 0, reverse=True)
|
||||||
|
bm25_results.sort(key=lambda r: r.bm25_score if hasattr(r, 'bm25_score') else 0, reverse=True)
|
||||||
|
graph_results.sort(key=lambda r: r.activation if hasattr(r, 'activation') else 0, reverse=True)
|
||||||
|
if temporal_results:
|
||||||
|
temporal_results.sort(key=lambda r: r.combined_score if hasattr(r, 'combined_score') else 0, reverse=True)
|
||||||
|
|
||||||
retrieval_duration = time.time() - retrieval_start
|
retrieval_duration = time.time() - retrieval_start
|
||||||
|
|
||||||
step_duration = time.time() - step_start
|
step_duration = time.time() - step_start
|
||||||
|
|
@ -1206,8 +1257,15 @@ class MemoryEngine:
|
||||||
})
|
})
|
||||||
log_buffer.append(f" [7] Queued access count updates for {len(visited_ids)} nodes")
|
log_buffer.append(f" [7] Queued access count updates for {len(visited_ids)} nodes")
|
||||||
|
|
||||||
|
# Log fact_type distribution in results
|
||||||
|
fact_type_counts = {}
|
||||||
|
for sr in top_scored:
|
||||||
|
ft = sr.retrieval.fact_type
|
||||||
|
fact_type_counts[ft] = fact_type_counts.get(ft, 0) + 1
|
||||||
|
|
||||||
total_time = time.time() - search_start
|
total_time = time.time() - search_start
|
||||||
log_buffer.append(f"[SEARCH {search_id}] Complete: {len(top_scored)} results ({total_tokens} tokens) in {total_time:.3f}s")
|
fact_type_summary = ", ".join([f"{ft}={count}" for ft, count in sorted(fact_type_counts.items())])
|
||||||
|
log_buffer.append(f"[SEARCH {search_id}] Complete: {len(top_scored)} results ({fact_type_summary}) ({total_tokens} tokens) in {total_time:.3f}s")
|
||||||
|
|
||||||
# Log all buffered logs at once
|
# Log all buffered logs at once
|
||||||
logger.info("\n" + "\n".join(log_buffer))
|
logger.info("\n" + "\n".join(log_buffer))
|
||||||
|
|
@ -1634,7 +1692,7 @@ class MemoryEngine:
|
||||||
where_clause = "WHERE " + " AND ".join(query_conditions) if query_conditions else ""
|
where_clause = "WHERE " + " AND ".join(query_conditions) if query_conditions else ""
|
||||||
|
|
||||||
units = await conn.fetch(f"""
|
units = await conn.fetch(f"""
|
||||||
SELECT id, text, event_date, context, occurred_start, occurred_end, mentioned_at, document_id
|
SELECT id, text, event_date, context, occurred_start, occurred_end, mentioned_at, document_id, chunk_id, fact_type
|
||||||
FROM memory_units
|
FROM memory_units
|
||||||
{where_clause}
|
{where_clause}
|
||||||
ORDER BY mentioned_at DESC NULLS LAST, event_date DESC
|
ORDER BY mentioned_at DESC NULLS LAST, event_date DESC
|
||||||
|
|
@ -1758,7 +1816,9 @@ class MemoryEngine:
|
||||||
"mentioned_at": row['mentioned_at'].isoformat() if row['mentioned_at'] else None,
|
"mentioned_at": row['mentioned_at'].isoformat() if row['mentioned_at'] else None,
|
||||||
"date": row['event_date'].strftime("%Y-%m-%d %H:%M") if row['event_date'] else "N/A", # Deprecated, kept for backwards compatibility
|
"date": row['event_date'].strftime("%Y-%m-%d %H:%M") if row['event_date'] else "N/A", # Deprecated, kept for backwards compatibility
|
||||||
"entities": ", ".join(entities) if entities else "None",
|
"entities": ", ".join(entities) if entities else "None",
|
||||||
"document_id": row['document_id']
|
"document_id": row['document_id'],
|
||||||
|
"chunk_id": row['chunk_id'] if row['chunk_id'] else None,
|
||||||
|
"fact_type": row['fact_type']
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1833,7 +1893,7 @@ class MemoryEngine:
|
||||||
query_params.append(offset)
|
query_params.append(offset)
|
||||||
|
|
||||||
units = await conn.fetch(f"""
|
units = await conn.fetch(f"""
|
||||||
SELECT id, text, event_date, context, fact_type, mentioned_at, occurred_start, occurred_end
|
SELECT id, text, event_date, context, fact_type, mentioned_at, occurred_start, occurred_end, chunk_id
|
||||||
FROM memory_units
|
FROM memory_units
|
||||||
{where_clause}
|
{where_clause}
|
||||||
ORDER BY mentioned_at DESC NULLS LAST, created_at DESC
|
ORDER BY mentioned_at DESC NULLS LAST, created_at DESC
|
||||||
|
|
@ -1877,7 +1937,8 @@ class MemoryEngine:
|
||||||
"mentioned_at": row['mentioned_at'].isoformat() if row['mentioned_at'] else None,
|
"mentioned_at": row['mentioned_at'].isoformat() if row['mentioned_at'] else None,
|
||||||
"occurred_start": row['occurred_start'].isoformat() if row['occurred_start'] else None,
|
"occurred_start": row['occurred_start'].isoformat() if row['occurred_start'] else None,
|
||||||
"occurred_end": row['occurred_end'].isoformat() if row['occurred_end'] else None,
|
"occurred_end": row['occurred_end'].isoformat() if row['occurred_end'] else None,
|
||||||
"entities": ", ".join(entities) if entities else ""
|
"entities": ", ".join(entities) if entities else "",
|
||||||
|
"chunk_id": row['chunk_id'] if row['chunk_id'] else None
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1950,7 +2011,8 @@ class MemoryEngine:
|
||||||
content_hash,
|
content_hash,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
LENGTH(original_text) as text_length
|
LENGTH(original_text) as text_length,
|
||||||
|
retain_params
|
||||||
FROM documents
|
FROM documents
|
||||||
{where_clause}
|
{where_clause}
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
|
|
@ -1998,7 +2060,8 @@ class MemoryEngine:
|
||||||
"created_at": row['created_at'].isoformat() if row['created_at'] else "",
|
"created_at": row['created_at'].isoformat() if row['created_at'] else "",
|
||||||
"updated_at": row['updated_at'].isoformat() if row['updated_at'] else "",
|
"updated_at": row['updated_at'].isoformat() if row['updated_at'] else "",
|
||||||
"text_length": row['text_length'] or 0,
|
"text_length": row['text_length'] or 0,
|
||||||
"memory_unit_count": unit_count
|
"memory_unit_count": unit_count,
|
||||||
|
"retain_params": row['retain_params'] if row['retain_params'] else None
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -2032,7 +2095,8 @@ class MemoryEngine:
|
||||||
original_text,
|
original_text,
|
||||||
content_hash,
|
content_hash,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at,
|
||||||
|
retain_params
|
||||||
FROM documents
|
FROM documents
|
||||||
WHERE id = $1 AND bank_id = $2
|
WHERE id = $1 AND bank_id = $2
|
||||||
""", document_id, bank_id)
|
""", document_id, bank_id)
|
||||||
|
|
@ -2054,7 +2118,47 @@ class MemoryEngine:
|
||||||
"content_hash": doc['content_hash'],
|
"content_hash": doc['content_hash'],
|
||||||
"created_at": doc['created_at'].isoformat() if doc['created_at'] else "",
|
"created_at": doc['created_at'].isoformat() if doc['created_at'] else "",
|
||||||
"updated_at": doc['updated_at'].isoformat() if doc['updated_at'] else "",
|
"updated_at": doc['updated_at'].isoformat() if doc['updated_at'] else "",
|
||||||
"memory_unit_count": unit_count_row['unit_count'] if unit_count_row else 0
|
"memory_unit_count": unit_count_row['unit_count'] if unit_count_row else 0,
|
||||||
|
"retain_params": doc['retain_params'] if doc['retain_params'] else None
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_chunk(
|
||||||
|
self,
|
||||||
|
chunk_id: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get a specific chunk by its ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chunk_id: Chunk ID (format: bank_id_document_id_chunk_index)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with chunk details including chunk_text, or None if not found
|
||||||
|
"""
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with acquire_with_retry(pool) as conn:
|
||||||
|
chunk = await conn.fetchrow("""
|
||||||
|
SELECT
|
||||||
|
chunk_id,
|
||||||
|
document_id,
|
||||||
|
bank_id,
|
||||||
|
chunk_index,
|
||||||
|
chunk_text,
|
||||||
|
created_at
|
||||||
|
FROM chunks
|
||||||
|
WHERE chunk_id = $1
|
||||||
|
""", chunk_id)
|
||||||
|
|
||||||
|
if not chunk:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"chunk_id": chunk['chunk_id'],
|
||||||
|
"document_id": chunk['document_id'],
|
||||||
|
"bank_id": chunk['bank_id'],
|
||||||
|
"chunk_index": chunk['chunk_index'],
|
||||||
|
"chunk_text": chunk['chunk_text'],
|
||||||
|
"created_at": chunk['created_at'].isoformat() if chunk['created_at'] else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _evaluate_opinion_update_async(
|
async def _evaluate_opinion_update_async(
|
||||||
|
|
@ -2792,24 +2896,127 @@ Guidelines:
|
||||||
logger.info(f"[OBSERVATIONS] {entity_name}: {len(facts)} facts -> {len(created_ids)} observations")
|
logger.info(f"[OBSERVATIONS] {entity_name}: {len(facts)} facts -> {len(created_ids)} observations")
|
||||||
return created_ids
|
return created_ids
|
||||||
|
|
||||||
|
async def _regenerate_observations_sync(
|
||||||
|
self,
|
||||||
|
bank_id: str,
|
||||||
|
entity_ids: List[str],
|
||||||
|
min_facts: int = 5
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Regenerate observations for entities synchronously (called during retain).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bank_id: Bank identifier
|
||||||
|
entity_ids: List of entity IDs to process
|
||||||
|
min_facts: Minimum facts required to regenerate observations
|
||||||
|
"""
|
||||||
|
if not bank_id or not entity_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
for entity_id in entity_ids:
|
||||||
|
try:
|
||||||
|
entity_uuid = uuid.UUID(entity_id) if isinstance(entity_id, str) else entity_id
|
||||||
|
|
||||||
|
# Check if entity exists
|
||||||
|
entity_exists = await conn.fetchrow(
|
||||||
|
"SELECT canonical_name FROM entities WHERE id = $1 AND bank_id = $2",
|
||||||
|
entity_uuid, bank_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not entity_exists:
|
||||||
|
logger.debug(f"[OBSERVATIONS] Entity {entity_id} not yet in bank {bank_id}, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
entity_name = entity_exists['canonical_name']
|
||||||
|
|
||||||
|
# Count facts linked to this entity
|
||||||
|
fact_count = await conn.fetchval(
|
||||||
|
"SELECT COUNT(*) FROM unit_entities WHERE entity_id = $1",
|
||||||
|
entity_uuid
|
||||||
|
) or 0
|
||||||
|
|
||||||
|
# Only regenerate if entity has enough facts
|
||||||
|
if fact_count >= min_facts:
|
||||||
|
await self.regenerate_entity_observations(bank_id, entity_id, entity_name, version=None)
|
||||||
|
else:
|
||||||
|
logger.debug(f"[OBSERVATIONS] Skipping {entity_name} ({fact_count} facts < {min_facts} threshold)")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[OBSERVATIONS] Error processing entity {entity_id}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
async def _handle_regenerate_observations(self, task_dict: Dict[str, Any]):
|
async def _handle_regenerate_observations(self, task_dict: Dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
Handler for regenerate_observations tasks.
|
Handler for regenerate_observations tasks.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task_dict: Dict with 'bank_id', 'entity_id', 'entity_name', 'version'
|
task_dict: Dict with 'bank_id' and either:
|
||||||
|
- 'entity_ids' (list): Process multiple entities
|
||||||
|
- 'entity_id', 'entity_name': Process single entity (legacy)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
bank_id = task_dict.get('bank_id')
|
bank_id = task_dict.get('bank_id')
|
||||||
|
|
||||||
|
# New format: multiple entity_ids
|
||||||
|
if 'entity_ids' in task_dict:
|
||||||
|
entity_ids = task_dict.get('entity_ids', [])
|
||||||
|
min_facts = task_dict.get('min_facts', 5)
|
||||||
|
|
||||||
|
if not bank_id or not entity_ids:
|
||||||
|
logger.error(f"[OBSERVATIONS] Missing required fields in task: {task_dict}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process each entity
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
for entity_id in entity_ids:
|
||||||
|
try:
|
||||||
|
# Fetch entity name and check fact count
|
||||||
|
import uuid as uuid_module
|
||||||
|
entity_uuid = uuid_module.UUID(entity_id) if isinstance(entity_id, str) else entity_id
|
||||||
|
|
||||||
|
# First check if entity exists
|
||||||
|
entity_exists = await conn.fetchrow(
|
||||||
|
"SELECT canonical_name FROM entities WHERE id = $1 AND bank_id = $2",
|
||||||
|
entity_uuid, bank_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not entity_exists:
|
||||||
|
logger.debug(f"[OBSERVATIONS] Entity {entity_id} not yet in bank {bank_id}, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
entity_name = entity_exists['canonical_name']
|
||||||
|
|
||||||
|
# Count facts linked to this entity
|
||||||
|
fact_count = await conn.fetchval(
|
||||||
|
"SELECT COUNT(*) FROM unit_entities WHERE entity_id = $1",
|
||||||
|
entity_uuid
|
||||||
|
) or 0
|
||||||
|
|
||||||
|
# Only regenerate if entity has enough facts
|
||||||
|
if fact_count >= min_facts:
|
||||||
|
await self.regenerate_entity_observations(bank_id, entity_id, entity_name, version=None)
|
||||||
|
else:
|
||||||
|
logger.debug(f"[OBSERVATIONS] Skipping {entity_name} ({fact_count} facts < {min_facts} threshold)")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[OBSERVATIONS] Error processing entity {entity_id}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Legacy format: single entity
|
||||||
|
else:
|
||||||
entity_id = task_dict.get('entity_id')
|
entity_id = task_dict.get('entity_id')
|
||||||
entity_name = task_dict.get('entity_name')
|
entity_name = task_dict.get('entity_name')
|
||||||
version = task_dict.get('version') # last_seen timestamp for deduplication
|
version = task_dict.get('version')
|
||||||
|
|
||||||
if not all([bank_id, entity_id, entity_name]):
|
if not all([bank_id, entity_id, entity_name]):
|
||||||
logger.error(f"[OBSERVATIONS] Missing required fields in task: {task_dict}")
|
logger.error(f"[OBSERVATIONS] Missing required fields in task: {task_dict}")
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.regenerate_entity_observations(bank_id, entity_id, entity_name, version)
|
await self.regenerate_entity_observations(bank_id, entity_id, entity_name, version)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[OBSERVATIONS] Error regenerating observations: {e}")
|
logger.error(f"[OBSERVATIONS] Error regenerating observations: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ async def check_duplicates_batch(
|
||||||
# Use occurred_start if available, otherwise use mentioned_at
|
# Use occurred_start if available, otherwise use mentioned_at
|
||||||
# For deduplication purposes, we need a time reference
|
# For deduplication purposes, we need a time reference
|
||||||
fact_date = fact.occurred_start if fact.occurred_start is not None else fact.mentioned_at
|
fact_date = fact.occurred_start if fact.occurred_start is not None else fact.mentioned_at
|
||||||
|
|
||||||
|
# Defensive: if both are None (shouldn't happen), use now()
|
||||||
|
if fact_date is None:
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
fact_date = datetime.now(timezone.utc)
|
||||||
|
|
||||||
# Round to 12-hour bucket to group similar times
|
# Round to 12-hour bucket to group similar times
|
||||||
bucket_key = fact_date.replace(
|
bucket_key = fact_date.replace(
|
||||||
hour=(fact_date.hour // 12) * 12,
|
hour=(fact_date.hour // 12) * 12,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ async def process_entities_batch(
|
||||||
conn,
|
conn,
|
||||||
bank_id: str,
|
bank_id: str,
|
||||||
unit_ids: List[str],
|
unit_ids: List[str],
|
||||||
facts: List[ProcessedFact]
|
facts: List[ProcessedFact],
|
||||||
|
log_buffer: List[str] = None
|
||||||
) -> List[Tuple[str, str, float]]:
|
) -> List[Tuple[str, str, float]]:
|
||||||
"""
|
"""
|
||||||
Process entities for all facts and create entity links.
|
Process entities for all facts and create entity links.
|
||||||
|
|
@ -35,6 +36,7 @@ async def process_entities_batch(
|
||||||
bank_id: Bank identifier
|
bank_id: Bank identifier
|
||||||
unit_ids: List of unit IDs (same length as facts)
|
unit_ids: List of unit IDs (same length as facts)
|
||||||
facts: List of ProcessedFact objects
|
facts: List of ProcessedFact objects
|
||||||
|
log_buffer: Optional buffer for detailed logging
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of entity link tuples: (unit_id, entity_id, confidence)
|
List of entity link tuples: (unit_id, entity_id, confidence)
|
||||||
|
|
@ -65,7 +67,7 @@ async def process_entities_batch(
|
||||||
"", # context (not used in current implementation)
|
"", # context (not used in current implementation)
|
||||||
fact_dates,
|
fact_dates,
|
||||||
entities_per_fact,
|
entities_per_fact,
|
||||||
[] # log_buffer (optional)
|
log_buffer # Pass log_buffer for detailed logging
|
||||||
)
|
)
|
||||||
|
|
||||||
return entity_links
|
return entity_links
|
||||||
|
|
|
||||||
|
|
@ -28,24 +28,20 @@ class Fact(BaseModel):
|
||||||
Final fact model for storage - built from lenient parsing of LLM response.
|
Final fact model for storage - built from lenient parsing of LLM response.
|
||||||
|
|
||||||
This is what fact_extraction returns and what the rest of the pipeline expects.
|
This is what fact_extraction returns and what the rest of the pipeline expects.
|
||||||
Only includes fields with meaningful values - nulls/empties are omitted.
|
Combined fact text format: "what | when | where | who | why"
|
||||||
"""
|
"""
|
||||||
# Required fields
|
# Required fields
|
||||||
fact: str = Field(description="Combined fact text from all dimensions")
|
fact: str = Field(description="Combined fact text: what | when | where | who | why")
|
||||||
fact_type: Literal["world", "bank", "opinion"] = Field(description="Perspective: world/bank/opinion")
|
fact_type: Literal["world", "bank", "opinion"] = Field(description="Perspective: world/bank/opinion")
|
||||||
|
|
||||||
# Optional dimension fields
|
|
||||||
emotional_significance: Optional[str] = None
|
|
||||||
reasoning_motivation: Optional[str] = None
|
|
||||||
preferences_opinions: Optional[str] = None
|
|
||||||
sensory_details: Optional[str] = None
|
|
||||||
observations: Optional[str] = None
|
|
||||||
|
|
||||||
# Optional temporal fields
|
# Optional temporal fields
|
||||||
occurred_start: Optional[str] = None
|
occurred_start: Optional[str] = None
|
||||||
occurred_end: Optional[str] = None
|
occurred_end: Optional[str] = None
|
||||||
mentioned_at: Optional[str] = None
|
mentioned_at: Optional[str] = None
|
||||||
|
|
||||||
|
# Optional location field
|
||||||
|
where: Optional[str] = Field(None, description="WHERE the fact occurred or is about (specific location, place, or area)")
|
||||||
|
|
||||||
# Optional structured data
|
# Optional structured data
|
||||||
entities: Optional[List[Entity]] = None
|
entities: Optional[List[Entity]] = None
|
||||||
causal_relations: Optional[List['CausalRelation']] = None
|
causal_relations: Optional[List['CausalRelation']] = None
|
||||||
|
|
@ -74,75 +70,93 @@ class CausalRelation(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class ExtractedFact(BaseModel):
|
class ExtractedFact(BaseModel):
|
||||||
"""A single extracted fact with structured dimensions for comprehensive capture."""
|
"""A single extracted fact with 5 required dimensions for comprehensive capture."""
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
json_schema_mode="validation",
|
json_schema_mode="validation",
|
||||||
# Only require truly critical fields - be lenient with everything else
|
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"required": ["factual_core", "fact_type"]
|
"required": ["what", "when", "where", "who", "why", "fact_type"]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Core factual dimension (CRITICAL - required)
|
# ==========================================================================
|
||||||
factual_core: str = Field(
|
# FIVE REQUIRED DIMENSIONS - LLM must think about each one
|
||||||
description="ACTUAL FACTS - what literally happened/was said. MUST be a complete, grammatically correct sentence with subject and verb. Capture WHAT was said, not just THAT something was said! 'Gina said Jon is the perfect mentor with positivity and determination' NOT 'Jon received encouragement'. Preserve: compliments, assessments, descriptions, key phrases. Be specific!"
|
# ==========================================================================
|
||||||
|
|
||||||
|
what: str = Field(
|
||||||
|
description="WHAT happened - COMPLETE, DETAILED description with ALL specifics. "
|
||||||
|
"NEVER summarize or omit details. Include: exact actions, objects, quantities, specifics. "
|
||||||
|
"BE VERBOSE - capture every detail that was mentioned. "
|
||||||
|
"Example: 'Emily got married to Sarah at a rooftop garden ceremony with 50 guests attending and a live jazz band playing' "
|
||||||
|
"NOT: 'A wedding happened' or 'Emily got married'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Optional dimensions - only include if present in the text
|
when: str = Field(
|
||||||
# CRITICAL: Each dimension MUST be a complete, standalone sentence that reads naturally
|
description="WHEN it happened - ALWAYS include temporal information if mentioned. "
|
||||||
emotional_significance: Optional[str] = Field(
|
"Include: specific dates, times, durations, relative time references. "
|
||||||
default=None,
|
"Examples: 'on June 15th, 2024 at 3pm', 'last weekend', 'for the past 3 years', 'every morning at 6am'. "
|
||||||
description="Emotions, feelings, personal meaning as a COMPLETE SENTENCE. Include subject + emotion/feeling. Examples: 'Sarah felt thrilled about the promotion', 'This was her favorite memory from childhood', 'The experience was magical for everyone involved', 'John found the loss devastating', 'She considers this her proudest moment'"
|
"Write 'N/A' ONLY if absolutely no temporal context exists. Prefer converting to absolute dates when possible."
|
||||||
)
|
|
||||||
reasoning_motivation: Optional[str] = Field(
|
|
||||||
default=None,
|
|
||||||
description="WHY it happened as a COMPLETE SENTENCE. Include subject + motivation/reason. Examples: 'She did this because she wanted to celebrate', 'He wrote the book to cope with grief', 'She was motivated by curiosity about the topic'"
|
|
||||||
)
|
|
||||||
preferences_opinions: Optional[str] = Field(
|
|
||||||
default=None,
|
|
||||||
description="Likes, dislikes, beliefs, values as a COMPLETE SENTENCE. Include subject + preference/opinion. Examples: 'Sarah loves coffee and drinks it daily', 'He thinks AI is transformative technology', 'She prefers working remotely over office work'"
|
|
||||||
)
|
|
||||||
sensory_details: Optional[str] = Field(
|
|
||||||
default=None,
|
|
||||||
description="Visual, auditory, physical descriptions as a COMPLETE SENTENCE. Include subject + descriptive details. USE EXACT WORDS from text! Examples: 'She has bright orange hair', 'The dancer moved so gracefully on stage', 'The beach was awesome', 'The movie had epic visuals', 'The water was freezing cold'"
|
|
||||||
)
|
|
||||||
observations: Optional[str] = Field(
|
|
||||||
default=None,
|
|
||||||
description="Observations, inferences, and specific details/metrics as a COMPLETE SENTENCE. Include subject + observed fact. Use this to capture: background facts, achievements, metrics, personal records, skills. Examples: 'Calvin traveled to Miami for the shoot', 'Gina won dance trophies in competitions', 'She knows programming from previous projects', 'User's personal best 5K time is 25:50', 'Sarah has completed 15 marathons', 'He speaks three languages fluently'"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fact kind - optional hint for LLM thinking, not critical for extraction
|
where: str = Field(
|
||||||
# We don't strictly validate this since it's just guidance for temporal handling
|
description="WHERE it happened or is about - SPECIFIC locations, places, areas, regions if applicable. "
|
||||||
fact_kind: Optional[str] = Field(
|
"Include: cities, neighborhoods, venues, buildings, countries, specific addresses when mentioned. "
|
||||||
|
"Examples: 'downtown San Francisco at a rooftop garden venue', 'at the user's home in Brooklyn', 'online via Zoom', 'Paris, France'. "
|
||||||
|
"Write 'N/A' ONLY if absolutely no location context exists or if the fact is completely location-agnostic."
|
||||||
|
)
|
||||||
|
|
||||||
|
who: str = Field(
|
||||||
|
description="WHO is involved - ALL people/entities with FULL context and relationships. "
|
||||||
|
"Include: names, roles, relationships to user, background details. "
|
||||||
|
"Resolve coreferences (if 'my roommate' is later named 'Emily', write 'Emily, the user's college roommate'). "
|
||||||
|
"BE DETAILED about relationships and roles. "
|
||||||
|
"Example: 'Emily (user's college roommate from Stanford, now works at Google), Sarah (Emily's partner of 5 years, software engineer)' "
|
||||||
|
"NOT: 'my friend' or 'Emily and Sarah'"
|
||||||
|
)
|
||||||
|
|
||||||
|
why: str = Field(
|
||||||
|
description="WHY it matters - ALL emotional, contextual, and motivational details. "
|
||||||
|
"Include EVERYTHING: feelings, preferences, motivations, observations, context, background, significance. "
|
||||||
|
"BE VERBOSE - capture all the nuance and meaning. "
|
||||||
|
"FOR ASSISTANT FACTS: MUST include what the user asked/requested that led to this interaction! "
|
||||||
|
"Example (world): 'The user felt thrilled and inspired, has always dreamed of an outdoor ceremony, mentioned wanting a similar garden venue, was particularly moved by the intimate atmosphere and personal vows' "
|
||||||
|
"Example (assistant): 'User asked how to fix slow API performance with 1000+ concurrent users, expected 70-80% reduction in database load' "
|
||||||
|
"NOT: 'User liked it' or 'To help user'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# CLASSIFICATION
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
fact_kind: str = Field(
|
||||||
default="conversation",
|
default="conversation",
|
||||||
description="Optional hint: 'conversation' = general info, 'event' = specific datable occurrence, 'other' = anything else. Helps determine if occurred dates should be set, but not critical."
|
description="'event' = specific datable occurrence (set occurred dates), 'conversation' = general info (no occurred dates)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Temporal fields - optional
|
# Temporal fields - optional
|
||||||
occurred_start: Optional[str] = Field(
|
occurred_start: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="WHEN THE EVENT ACTUALLY HAPPENED (not when mentioned). ISO timestamp. For datable events only (fact_kind='event'). Examples: 'went to Tokyo last spring' on June 10 → occurred_start='2024-03-01' (spring start), 'accident yesterday' on March 15 → occurred_start='2024-03-14' (yesterday). Leave null for general info (fact_kind='conversation')."
|
description="WHEN the event happened (ISO timestamp). Only for fact_kind='event'. Leave null for conversations."
|
||||||
)
|
)
|
||||||
occurred_end: Optional[str] = Field(
|
occurred_end: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="WHEN THE EVENT ACTUALLY ENDED (not when mentioned). ISO timestamp. For datable events with duration (fact_kind='event'). Examples: 'went to Tokyo last spring' → occurred_end='2024-05-31' (spring end). Can be same as occurred_start for single-day events. Leave null for general info."
|
description="WHEN the event ended (ISO timestamp). Only for events with duration. Leave null for conversations."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Classification (CRITICAL - required)
|
# Classification (CRITICAL - required)
|
||||||
# Note: LLM uses "assistant" but we convert to "bank" for storage
|
# Note: LLM uses "assistant" but we convert to "bank" for storage
|
||||||
fact_type: Literal["world", "assistant"] = Field(
|
fact_type: Literal["world", "assistant"] = Field(
|
||||||
description="REQUIRED: 'world' = everything NOT involving the assistant (user's background, skills, experiences, other people's lives, events). 'assistant' = interactions BY or TO the assistant (user asked assistant, assistant recommended, assistant helped user, etc.)"
|
description="'world' = about the user/others (background, experiences). 'assistant' = interactions with the assistant."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Entities and relations
|
# Entities - extracted from 'who' field
|
||||||
entities: Optional[List[Entity]] = Field(
|
entities: Optional[List[Entity]] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="ONLY specific, named entities worth tracking: people's names (e.g., 'Sarah', 'Dr. Smith'), organizations (e.g., 'Google', 'MIT'), specific places (e.g., 'Paris', 'Central Park'). DO NOT include: generic relations (mom, friend, boss, colleague), common nouns (apple, car, house), pronouns (he, she), or vague references (someone, a guy). Can be null or empty list [] if no entities."
|
description="Named entities from 'who': people names, organizations, places. NOT generic relations."
|
||||||
)
|
)
|
||||||
causal_relations: Optional[List[CausalRelation]] = Field(
|
causal_relations: Optional[List[CausalRelation]] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Causal links to other facts in this batch. Example: fact about rain causes fact about cancelled game. Can be null or empty list [] if no causal relations."
|
description="Causal links to other facts. Can be null."
|
||||||
)
|
)
|
||||||
|
|
||||||
@field_validator('entities', mode='before')
|
@field_validator('entities', mode='before')
|
||||||
|
|
@ -163,25 +177,20 @@ class ExtractedFact(BaseModel):
|
||||||
|
|
||||||
def build_fact_text(self) -> str:
|
def build_fact_text(self) -> str:
|
||||||
"""Combine all dimensions into a single comprehensive fact string."""
|
"""Combine all dimensions into a single comprehensive fact string."""
|
||||||
parts = [self.factual_core]
|
parts = [self.what]
|
||||||
|
|
||||||
if self.emotional_significance:
|
# Add 'who' if not N/A
|
||||||
parts.append(self.emotional_significance)
|
if self.who and self.who.upper() != 'N/A':
|
||||||
if self.reasoning_motivation:
|
parts.append(f"Involving: {self.who}")
|
||||||
parts.append(self.reasoning_motivation)
|
|
||||||
if self.preferences_opinions:
|
# Add 'why' if not N/A
|
||||||
parts.append(self.preferences_opinions)
|
if self.why and self.why.upper() != 'N/A':
|
||||||
if self.sensory_details:
|
parts.append(self.why)
|
||||||
parts.append(self.sensory_details)
|
|
||||||
if self.observations:
|
|
||||||
parts.append(self.observations)
|
|
||||||
|
|
||||||
# Join with appropriate connectors
|
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
return parts[0]
|
return parts[0]
|
||||||
|
|
||||||
# Combine: "Core fact - emotional/significance context"
|
return " | ".join(parts)
|
||||||
return f"{parts[0]} - {' - '.join(parts[1:])}"
|
|
||||||
|
|
||||||
|
|
||||||
class FactExtractionResponse(BaseModel):
|
class FactExtractionResponse(BaseModel):
|
||||||
|
|
@ -193,27 +202,35 @@ class FactExtractionResponse(BaseModel):
|
||||||
|
|
||||||
def chunk_text(text: str, max_chars: int) -> List[str]:
|
def chunk_text(text: str, max_chars: int) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Split text into chunks at sentence boundaries using LangChain's text splitter.
|
Split text into chunks, preserving conversation structure when possible.
|
||||||
|
|
||||||
Uses RecursiveCharacterTextSplitter which intelligently splits at sentence boundaries
|
For JSON conversation arrays (user/assistant turns), splits at turn boundaries
|
||||||
and allows chunks to slightly exceed max_chars to finish sentences naturally.
|
while preserving speaker context. For plain text, uses sentence-aware splitting.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Input text to chunk
|
text: Input text to chunk (plain text or JSON conversation)
|
||||||
max_chars: Maximum characters per chunk (default 120k ≈ 30k tokens)
|
max_chars: Maximum characters per chunk (default 120k ≈ 30k tokens)
|
||||||
Note: chunks may slightly exceed this to complete sentences
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of text chunks, roughly under max_chars
|
List of text chunks, roughly under max_chars
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||||
|
|
||||||
# If text is small enough, return as-is
|
# If text is small enough, return as-is
|
||||||
if len(text) <= max_chars:
|
if len(text) <= max_chars:
|
||||||
return [text]
|
return [text]
|
||||||
|
|
||||||
# Configure splitter to split at sentence boundaries first
|
# Try to parse as JSON conversation array
|
||||||
# Separators in order of preference: paragraphs, newlines, sentences, words
|
try:
|
||||||
|
parsed = json.loads(text)
|
||||||
|
if isinstance(parsed, list) and all(isinstance(turn, dict) for turn in parsed):
|
||||||
|
# This looks like a conversation - chunk at turn boundaries
|
||||||
|
return _chunk_conversation(parsed, max_chars)
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fall back to sentence-aware text splitting
|
||||||
splitter = RecursiveCharacterTextSplitter(
|
splitter = RecursiveCharacterTextSplitter(
|
||||||
chunk_size=max_chars,
|
chunk_size=max_chars,
|
||||||
chunk_overlap=0,
|
chunk_overlap=0,
|
||||||
|
|
@ -235,6 +252,45 @@ def chunk_text(text: str, max_chars: int) -> List[str]:
|
||||||
return splitter.split_text(text)
|
return splitter.split_text(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _chunk_conversation(turns: List[dict], max_chars: int) -> List[str]:
|
||||||
|
"""
|
||||||
|
Chunk a conversation array at turn boundaries, preserving complete turns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
turns: List of conversation turn dicts (with 'role' and 'content' keys)
|
||||||
|
max_chars: Maximum characters per chunk
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of JSON-serialized chunks, each containing complete turns
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
chunks = []
|
||||||
|
current_chunk = []
|
||||||
|
current_size = 2 # Account for "[]"
|
||||||
|
|
||||||
|
for turn in turns:
|
||||||
|
# Estimate size of this turn when serialized (with comma separator)
|
||||||
|
turn_json = json.dumps(turn, ensure_ascii=False)
|
||||||
|
turn_size = len(turn_json) + 1 # +1 for comma
|
||||||
|
|
||||||
|
# If adding this turn would exceed limit and we have turns, save current chunk
|
||||||
|
if current_size + turn_size > max_chars and current_chunk:
|
||||||
|
chunks.append(json.dumps(current_chunk, ensure_ascii=False))
|
||||||
|
current_chunk = []
|
||||||
|
current_size = 2 # Reset to "[]"
|
||||||
|
|
||||||
|
# Add turn to current chunk
|
||||||
|
current_chunk.append(turn)
|
||||||
|
current_size += turn_size
|
||||||
|
|
||||||
|
# Add final chunk if non-empty
|
||||||
|
if current_chunk:
|
||||||
|
chunks.append(json.dumps(current_chunk, ensure_ascii=False))
|
||||||
|
|
||||||
|
return chunks if chunks else [json.dumps(turns, ensure_ascii=False)]
|
||||||
|
|
||||||
|
|
||||||
async def _extract_facts_from_chunk(
|
async def _extract_facts_from_chunk(
|
||||||
chunk: str,
|
chunk: str,
|
||||||
chunk_index: int,
|
chunk_index: int,
|
||||||
|
|
@ -261,141 +317,145 @@ async def _extract_facts_from_chunk(
|
||||||
else:
|
else:
|
||||||
fact_types_instruction = "Extract ONLY 'world' and 'assistant' type facts. DO NOT extract opinions - those are extracted separately."
|
fact_types_instruction = "Extract ONLY 'world' and 'assistant' type facts. DO NOT extract opinions - those are extracted separately."
|
||||||
|
|
||||||
prompt = f"""Extract comprehensive facts from user text for an AI memory system.
|
prompt = f"""Extract facts from text into structured format with FOUR required dimensions - BE EXTREMELY DETAILED.
|
||||||
|
|
||||||
{fact_types_instruction}
|
{fact_types_instruction}
|
||||||
|
|
||||||
## CONTEXT
|
Context: {context if context else 'none'}{agent_context}
|
||||||
- Context: {context if context else 'none'}{agent_context}
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
══════════════════════════════════════════════════════════════════════════
|
||||||
SECTION 1: TEMPORAL HANDLING (CRITICAL)
|
FACT FORMAT - ALL FIVE DIMENSIONS REQUIRED - MAXIMUM VERBOSITY
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
### 1.1 DETECT TEMPORAL MARKERS
|
For EACH fact, CAPTURE ALL DETAILS - NEVER SUMMARIZE OR OMIT:
|
||||||
Watch for: "yesterday", "last week/month/year/summer", "ago", "tomorrow", "next", "happened", "occurred", past tense verbs ("went", "visited", "saw")
|
|
||||||
|
|
||||||
### 1.2 DUAL FACT CREATION (KEY RULE)
|
1. **what**: WHAT happened - COMPLETE description with ALL specifics (objects, actions, quantities, details)
|
||||||
When text mentions a past/future event → Create TWO facts:
|
2. **when**: WHEN it happened - ALWAYS include temporal info (dates, times, durations, relative times)
|
||||||
1. MENTION FACT: "On [context date], it was mentioned that..." (occurred_start = context date)
|
3. **where**: WHERE it happened or is about - SPECIFIC locations, places, areas, regions (if applicable)
|
||||||
2. EVENT FACT: "[Action] in [absolute date]" (occurred_start = actual event date)
|
4. **who**: WHO is involved - ALL people/entities with FULL relationships and background
|
||||||
|
5. **why**: WHY it matters - ALL emotions, preferences, motivations, significance, nuance
|
||||||
|
- For assistant facts: MUST include what the user asked/requested that triggered this!
|
||||||
|
|
||||||
### 1.3 ABSOLUTE DATE CONVERSION
|
Plus: fact_type, fact_kind, entities, occurred_start/end (for structured dates), where (structured location)
|
||||||
ALWAYS convert relative → absolute in factual_core text:
|
|
||||||
- "yesterday" → "on [date-1]"
|
|
||||||
- "last week" → "around [specific week]"
|
|
||||||
- "last summer" → "in summer [year] (June-August [year])"
|
|
||||||
- "next month" → "in [month name] [year]"
|
|
||||||
|
|
||||||
### 1.4 occurred_start/end FIELDS ⚠️ CRITICAL
|
VERBOSITY REQUIREMENT: Include EVERY detail mentioned. More detail is ALWAYS better than less.
|
||||||
|
|
||||||
**WHAT THEY REPRESENT:**
|
══════════════════════════════════════════════════════════════════════════
|
||||||
- occurred_start/end = WHEN THE EVENT ACTUALLY HAPPENED (NOT when it was mentioned!)
|
COREFERENCE RESOLUTION (CRITICAL)
|
||||||
- These answer: "When did this event occur in reality?"
|
══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
**WHEN TO SET THEM:**
|
When text uses BOTH a generic relation AND a name for the same person → LINK THEM!
|
||||||
✅ SET for datable events (fact_kind="event"):
|
|
||||||
- "went to Tokyo last spring" → occurred_start = March 1, 2024 (spring started)
|
|
||||||
- "accident yesterday" → occurred_start = context date - 1 day
|
|
||||||
- "party next Saturday" → occurred_start = next Saturday's date
|
|
||||||
|
|
||||||
❌ LEAVE NULL for general info (fact_kind="conversation"):
|
Example input: "I went to my college roommate's wedding last June. Emily finally married Sarah after 5 years together."
|
||||||
- "loves coffee" → no occurred dates (timeless preference)
|
|
||||||
- "works as engineer" → no occurred dates (ongoing state)
|
|
||||||
- "is expanding business" → no occurred dates (ongoing activity)
|
|
||||||
|
|
||||||
**KEY DISTINCTION:**
|
CORRECT output:
|
||||||
- occurred_start/end: When the event happened/will happen
|
- what: "Emily got married to Sarah at a rooftop garden ceremony"
|
||||||
- mentioned_at: When this was said/written (set automatically to context date)
|
- when: "in June 2024, after dating for 5 years"
|
||||||
- These are DIFFERENT! Example: On June 10, saying "went to Tokyo in March" → occurred_start=March, mentioned_at=June 10
|
- where: "downtown San Francisco, at a rooftop garden venue"
|
||||||
|
- who: "Emily (user's college roommate), Sarah (Emily's partner of 5 years)"
|
||||||
|
- why: "User found it romantic and beautiful, dreams of similar outdoor ceremony"
|
||||||
|
- where (structured): "San Francisco"
|
||||||
|
|
||||||
**FORMAT:** ISO timestamps "2024-06-15T00:00:00Z"
|
WRONG output:
|
||||||
|
- what: "User's roommate got married" ← LOSES THE NAME!
|
||||||
|
- who: "the roommate" ← WRONG - use the actual name!
|
||||||
|
- where: (missing) ← WRONG - include the location!
|
||||||
|
|
||||||
### 1.5 EXAMPLES - STUDY THESE CAREFULLY
|
══════════════════════════════════════════════════════════════════════════
|
||||||
|
TEMPORAL HANDLING
|
||||||
|
══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
**Example 1: "yesterday" temporal detection**
|
For EVENTS (fact_kind="event"):
|
||||||
Input (Context: March 15, 2024): "Hey Taylor! The volunteers were amazing yesterday. But something unexpected happened - a vehicle accident near the center. Everyone was okay though."
|
- Convert relative dates → absolute: "yesterday" on March 15 → "March 14, 2024"
|
||||||
|
- Set occurred_start/occurred_end to WHEN IT HAPPENED (not when mentioned)
|
||||||
|
|
||||||
Output (3 facts):
|
For CONVERSATIONS (fact_kind="conversation"):
|
||||||
1. factual_core: "On March 15, 2024, Alex told Taylor that the volunteers were amazing"
|
- General info, preferences, ongoing states → NO occurred dates
|
||||||
occurred_start: "2024-03-15T00:00:00Z", entities: ["Alex", "Taylor"]
|
- Examples: "loves coffee", "works as engineer"
|
||||||
|
|
||||||
2. factual_core: "On March 15, 2024, Alex mentioned that something unexpected happened the previous day - a vehicle accident"
|
══════════════════════════════════════════════════════════════════════════
|
||||||
occurred_start: "2024-03-15T00:00:00Z", entities: ["Alex"]
|
FACT TYPE
|
||||||
|
══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
3. factual_core: "On March 14, 2024, a vehicle accident occurred near the center, but everyone was okay"
|
- **world**: User's life, other people, events (would exist without this conversation)
|
||||||
occurred_start: "2024-03-14T00:00:00Z" ← THE ACTUAL EVENT DATE (yesterday from March 15)
|
- **assistant**: Interactions with assistant (requests, recommendations, help)
|
||||||
|
⚠️ CRITICAL for assistant facts: ALWAYS capture the user's request/question in the fact!
|
||||||
|
Include: what the user asked, what problem they wanted solved, what context they provided
|
||||||
|
|
||||||
**Example 2: "last spring" temporal detection**
|
══════════════════════════════════════════════════════════════════════════
|
||||||
Input (Context: June 10, 2024): "Casey went to Tokyo last spring. They had an incredible time visiting temples and trying authentic ramen."
|
USER PREFERENCES (CRITICAL)
|
||||||
|
══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Output (2 facts):
|
ALWAYS extract user preferences as separate facts! Watch for these keywords:
|
||||||
1. factual_core: "On June 10, 2024, it was mentioned that Casey went to Tokyo the previous spring"
|
- "enjoy", "like", "love", "prefer", "hate", "dislike", "favorite", "ideal", "dream", "want"
|
||||||
occurred_start: "2024-06-10T00:00:00Z", entities: ["Casey", "Tokyo"]
|
|
||||||
|
|
||||||
2. factual_core: "Casey went to Tokyo in spring 2024 (March-May 2024) and visited temples and tried authentic ramen"
|
Example: "I love Italian food and prefer outdoor dining"
|
||||||
occurred_start: "2024-03-01T00:00:00Z", occurred_end: "2024-05-31T23:59:59Z" ← THE ACTUAL EVENT DATES
|
→ Fact 1: what="User loves Italian food", who="user", why="This is a food preference", entities=["user"]
|
||||||
emotional_significance: "Casey had an incredible time in Tokyo"
|
→ Fact 2: what="User prefers outdoor dining", who="user", why="This is a dining preference", entities=["user"]
|
||||||
entities: ["Casey", "Tokyo"]
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
══════════════════════════════════════════════════════════════════════════
|
||||||
SECTION 2: EXTRACTION RULES
|
ENTITIES - INCLUDE "user" (CRITICAL)
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
### 2.1 WHAT TO EXTRACT
|
When a fact is ABOUT the user (their preferences, plans, experiences), ALWAYS include "user" in entities!
|
||||||
✅ User requests to assistant + assistant actions (extract separately)
|
|
||||||
✅ Preferences, recommendations, plans, activities, encouragement (with actual content)
|
|
||||||
✅ Possessions, achievements, metrics, skills, background facts
|
|
||||||
|
|
||||||
### 2.2 WHAT TO SKIP
|
✅ CORRECT: entities=["user"] for "User loves coffee"
|
||||||
❌ Greetings, filler ("thanks", "cool"), structural statements
|
✅ CORRECT: entities=["user", "Emily"] for "User attended Emily's wedding"
|
||||||
|
❌ WRONG: entities=[] for facts about the user
|
||||||
|
|
||||||
### 2.3 Q&A HANDLING
|
══════════════════════════════════════════════════════════════════════════
|
||||||
- Combine simple informational Q&A into one fact
|
EXAMPLES
|
||||||
- Split user requests to assistant into two facts (request + response)
|
══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
Example 1 - World Facts (Context: June 10, 2024):
|
||||||
SECTION 3: STRUCTURED DIMENSIONS
|
Input: "I'm planning my wedding and want a small outdoor ceremony. I just got back from my college roommate Emily's wedding - she married Sarah at a rooftop garden, it was so romantic!"
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
### 3.1 REQUIRED FIELD
|
Output facts:
|
||||||
- **factual_core**: Capture WHAT was said, not just THAT something was said. Complete sentence.
|
|
||||||
|
|
||||||
### 3.2 OPTIONAL FIELDS (use when present in text)
|
1. User's wedding preference
|
||||||
- **emotional_significance**: Emotions, feelings, qualitative descriptors. Complete sentence with subject.
|
- what: "User wants a small outdoor ceremony for their wedding"
|
||||||
- **reasoning_motivation**: Why it happened, intentions, goals. Complete sentence with subject.
|
- who: "user"
|
||||||
- **preferences_opinions**: Likes, dislikes, beliefs, values. Complete sentence with subject. Use for: "ideal", "favorite", "dream", "perfect"
|
- why: "User prefers intimate outdoor settings"
|
||||||
- **sensory_details**: Visual, auditory, physical descriptions. Complete sentence. USE EXACT WORDS from text!
|
- fact_type: "world", fact_kind: "conversation"
|
||||||
- **observations**: Background facts, possessions, achievements, metrics, skills. Complete sentence with subject.
|
- entities: ["user"]
|
||||||
|
|
||||||
### 3.3 FORMATTING RULE
|
2. User planning wedding
|
||||||
Each dimension MUST be a complete, grammatically correct sentence with subject that can stand alone.
|
- what: "User is planning their own wedding"
|
||||||
|
- who: "user"
|
||||||
|
- why: "Inspired by Emily's ceremony"
|
||||||
|
- fact_type: "world", fact_kind: "conversation"
|
||||||
|
- entities: ["user"]
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
3. Emily's wedding (THE EVENT)
|
||||||
SECTION 4: FACT CLASSIFICATION
|
- what: "Emily got married to Sarah at a rooftop garden ceremony in the city"
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
- who: "Emily (user's college roommate), Sarah (Emily's partner)"
|
||||||
|
- why: "User found it romantic and beautiful"
|
||||||
|
- fact_type: "world", fact_kind: "event"
|
||||||
|
- occurred_start: "2024-06-09T00:00:00Z" (recently, user "just got back")
|
||||||
|
- entities: ["user", "Emily", "Sarah"]
|
||||||
|
|
||||||
### 4.1 fact_kind (temporal nature)
|
Example 2 - Assistant Facts (Context: March 5, 2024):
|
||||||
- **conversation**: General info, ongoing activities (no occurred dates)
|
Input: "User: My API is really slow when we have 1000+ concurrent users. What can I do?
|
||||||
- **event**: Specific datable occurrence (MUST set occurred_start/end)
|
Assistant: I'd recommend implementing Redis for caching frequently-accessed data, which should reduce your database load by 70-80%."
|
||||||
- **other**: Catch-all
|
|
||||||
|
|
||||||
### 4.2 fact_type (subject matter)
|
Output fact:
|
||||||
- **world**: Everything NOT involving assistant (user background, other people, events)
|
- what: "Assistant recommended implementing Redis for caching frequently-accessed data to improve API performance"
|
||||||
- **assistant**: Interactions BY or TO assistant (requests, recommendations, actions in THIS conversation)
|
- when: "March 5, 2024 during conversation"
|
||||||
|
- who: "user, assistant"
|
||||||
|
- why: "User asked how to fix slow API performance with 1000+ concurrent users, expected 70-80% reduction in database load"
|
||||||
|
- fact_type: "assistant", fact_kind: "conversation"
|
||||||
|
- entities: ["user"]
|
||||||
|
|
||||||
Rule: If it would exist without this conversation → world. If only exists because of this conversation → assistant.
|
Note how the "why" field captures the FULL STORY: what the user asked AND what outcome was expected!
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
══════════════════════════════════════════════════════════════════════════
|
||||||
SECTION 5: ENTITIES & CAUSALITY
|
WHAT TO EXTRACT vs SKIP
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
✅ EXTRACT: User preferences (ALWAYS as separate facts!), feelings, plans, events, relationships, achievements
|
||||||
|
❌ SKIP: Greetings, filler ("thanks", "cool"), purely structural statements"""
|
||||||
|
|
||||||
### 5.1 ENTITIES
|
|
||||||
Extract: People names, organizations, specific places, products
|
|
||||||
Skip: Generic relations (mom, friend), pronouns, common nouns
|
|
||||||
|
|
||||||
### 5.2 CAUSAL RELATIONS
|
|
||||||
Link facts when explicit causation: causes, caused_by, enables, prevents"""
|
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -407,14 +467,16 @@ Link facts when explicit causation: causes, caused_by, enables, prevents"""
|
||||||
max_retries = 2
|
max_retries = 2
|
||||||
last_error = None
|
last_error = None
|
||||||
|
|
||||||
# inject all the chunk metadata for better reasoning
|
# Build user message with metadata and chunk content in a clear format
|
||||||
chunk_data = json.dumps({
|
user_message = f"""Extract facts from the following text chunk.
|
||||||
"chunk_index": chunk_index,
|
|
||||||
"total_chunks": total_chunks,
|
Chunk: {chunk_index + 1}/{total_chunks}
|
||||||
"event_date": event_date.isoformat(),
|
Event Date: {event_date.isoformat()}
|
||||||
"context": context,
|
Context: {context if context else 'none'}
|
||||||
"chunk_content": chunk
|
|
||||||
})
|
Text:
|
||||||
|
{chunk}"""
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
extraction_response_json = await llm_config.call(
|
extraction_response_json = await llm_config.call(
|
||||||
|
|
@ -425,7 +487,7 @@ Link facts when explicit causation: causes, caused_by, enables, prevents"""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": chunk_data
|
"content": user_message
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
response_format=FactExtractionResponse,
|
response_format=FactExtractionResponse,
|
||||||
|
|
@ -437,32 +499,58 @@ Link facts when explicit causation: causes, caused_by, enables, prevents"""
|
||||||
|
|
||||||
# Lenient parsing of facts from raw JSON
|
# Lenient parsing of facts from raw JSON
|
||||||
chunk_facts = []
|
chunk_facts = []
|
||||||
|
has_malformed_facts = False
|
||||||
|
|
||||||
# Handle malformed LLM responses
|
# Handle malformed LLM responses
|
||||||
if not isinstance(extraction_response_json, dict):
|
if not isinstance(extraction_response_json, dict):
|
||||||
|
if attempt < max_retries - 1:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"LLM returned non-dict JSON: {type(extraction_response_json).__name__}. "
|
f"LLM returned non-dict JSON on attempt {attempt + 1}/{max_retries}: {type(extraction_response_json).__name__}. Retrying..."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"LLM returned non-dict JSON after {max_retries} attempts: {type(extraction_response_json).__name__}. "
|
||||||
f"Raw: {str(extraction_response_json)[:500]}"
|
f"Raw: {str(extraction_response_json)[:500]}"
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
raw_facts = extraction_response_json.get('facts', [])
|
raw_facts = extraction_response_json.get('facts', [])
|
||||||
if not raw_facts:
|
if not raw_facts:
|
||||||
logger.warning(
|
logger.debug(
|
||||||
f"LLM response missing 'facts' field or returned empty list. "
|
f"LLM response missing 'facts' field or returned empty list. "
|
||||||
f"Response: {extraction_response_json}"
|
f"Response: {extraction_response_json}. "
|
||||||
|
f"Input: "
|
||||||
|
f"date: {event_date.isoformat()}, "
|
||||||
|
f"context: {context if context else 'none'}, "
|
||||||
|
f"text: {chunk}"
|
||||||
)
|
)
|
||||||
|
|
||||||
for i, llm_fact in enumerate(raw_facts):
|
for i, llm_fact in enumerate(raw_facts):
|
||||||
# Skip non-dict entries
|
# Skip non-dict entries but track them for retry
|
||||||
if not isinstance(llm_fact, dict):
|
if not isinstance(llm_fact, dict):
|
||||||
logger.warning(f"Skipping non-dict fact at index {i}")
|
logger.warning(f"Skipping non-dict fact at index {i}")
|
||||||
|
has_malformed_facts = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Critical field: factual_core (MUST have this)
|
# Helper to get non-empty value
|
||||||
factual_core = llm_fact.get('factual_core')
|
def get_value(field_name):
|
||||||
if not factual_core:
|
value = llm_fact.get(field_name)
|
||||||
logger.warning(f"Skipping fact {i}: missing factual_core")
|
if value and value != '' and value != [] and value != {} and str(value).upper() != 'N/A':
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
# NEW FORMAT: what, when, who, why (all required)
|
||||||
|
what = get_value('what')
|
||||||
|
when = get_value('when')
|
||||||
|
who = get_value('who')
|
||||||
|
why = get_value('why')
|
||||||
|
|
||||||
|
# Fallback to old format if new fields not present
|
||||||
|
if not what:
|
||||||
|
what = get_value('factual_core')
|
||||||
|
if not what:
|
||||||
|
logger.warning(f"Skipping fact {i}: missing 'what' field")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Critical field: fact_type
|
# Critical field: fact_type
|
||||||
|
|
@ -491,34 +579,20 @@ Link facts when explicit causation: causes, caused_by, enables, prevents"""
|
||||||
if fact_kind not in ['conversation', 'event', 'other']:
|
if fact_kind not in ['conversation', 'event', 'other']:
|
||||||
fact_kind = 'conversation'
|
fact_kind = 'conversation'
|
||||||
|
|
||||||
# Build combined fact text from dimensions
|
# Build combined fact text from the 4 dimensions: what | when | who | why
|
||||||
dimension_parts = []
|
|
||||||
fact_data = {}
|
fact_data = {}
|
||||||
|
combined_parts = [what]
|
||||||
|
|
||||||
# Helper to get non-empty value
|
if when:
|
||||||
def get_value(field_name):
|
combined_parts.append(f"When: {when}")
|
||||||
value = llm_fact.get(field_name)
|
|
||||||
if value and value != '' and value != [] and value != {}:
|
|
||||||
return value
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Collect dimension fields
|
if who:
|
||||||
for field in ['emotional_significance', 'reasoning_motivation', 'preferences_opinions',
|
combined_parts.append(f"Involving: {who}")
|
||||||
'sensory_details', 'observations']:
|
|
||||||
value = get_value(field)
|
|
||||||
if value:
|
|
||||||
# Handle case where LLM returns list instead of string
|
|
||||||
if isinstance(value, list):
|
|
||||||
value = '; '.join(str(v) for v in value)
|
|
||||||
fact_data[field] = value
|
|
||||||
dimension_parts.append(value)
|
|
||||||
|
|
||||||
# Build combined fact text
|
if why:
|
||||||
combined_parts = [factual_core] + dimension_parts
|
combined_parts.append(why)
|
||||||
if len(combined_parts) == 1:
|
|
||||||
combined_text = combined_parts[0]
|
combined_text = " | ".join(combined_parts)
|
||||||
else:
|
|
||||||
combined_text = f"{combined_parts[0]} - {' - '.join(combined_parts[1:])}"
|
|
||||||
|
|
||||||
# Add temporal fields
|
# Add temporal fields
|
||||||
# For events: occurred_start/occurred_end (when the event happened)
|
# For events: occurred_start/occurred_end (when the event happened)
|
||||||
|
|
@ -575,7 +649,16 @@ Link facts when explicit causation: causes, caused_by, enables, prevents"""
|
||||||
chunk_facts.append(fact)
|
chunk_facts.append(fact)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to create Fact model for fact {i}: {e}")
|
logger.error(f"Failed to create Fact model for fact {i}: {e}")
|
||||||
|
has_malformed_facts = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# If we got malformed facts and haven't exhausted retries, try again
|
||||||
|
if has_malformed_facts and len(chunk_facts) < len(raw_facts) * 0.8 and attempt < max_retries - 1:
|
||||||
|
logger.warning(
|
||||||
|
f"Got {len(raw_facts) - len(chunk_facts)} malformed facts out of {len(raw_facts)} on attempt {attempt + 1}/{max_retries}. Retrying..."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
return chunk_facts
|
return chunk_facts
|
||||||
|
|
||||||
except BadRequestError as e:
|
except BadRequestError as e:
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,8 @@ async def insert_facts_batch(
|
||||||
access_counts.append(0) # Initial access count
|
access_counts.append(0) # Initial access count
|
||||||
metadata_jsons.append(json.dumps(fact.metadata))
|
metadata_jsons.append(json.dumps(fact.metadata))
|
||||||
chunk_ids.append(fact.chunk_id)
|
chunk_ids.append(fact.chunk_id)
|
||||||
document_ids.append(document_id)
|
# Use per-fact document_id if available, otherwise fallback to batch-level document_id
|
||||||
|
document_ids.append(fact.document_id if fact.document_id else document_id)
|
||||||
|
|
||||||
# Batch insert all facts
|
# Batch insert all facts
|
||||||
results = await conn.fetch(
|
results = await conn.fetch(
|
||||||
|
|
@ -127,7 +128,8 @@ async def handle_document_tracking(
|
||||||
bank_id: str,
|
bank_id: str,
|
||||||
document_id: str,
|
document_id: str,
|
||||||
combined_content: str,
|
combined_content: str,
|
||||||
is_first_batch: bool
|
is_first_batch: bool,
|
||||||
|
retain_params: Optional[dict] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Handle document tracking in the database.
|
Handle document tracking in the database.
|
||||||
|
|
@ -138,6 +140,7 @@ async def handle_document_tracking(
|
||||||
document_id: Document identifier
|
document_id: Document identifier
|
||||||
combined_content: Combined content text from all content items
|
combined_content: Combined content text from all content items
|
||||||
is_first_batch: Whether this is the first batch (for chunked operations)
|
is_first_batch: Whether this is the first batch (for chunked operations)
|
||||||
|
retain_params: Optional parameters passed during retain (context, event_date, etc.)
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
|
@ -155,17 +158,19 @@ async def handle_document_tracking(
|
||||||
# Insert document (or update if exists from concurrent operations)
|
# Insert document (or update if exists from concurrent operations)
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO documents (id, bank_id, original_text, content_hash, metadata)
|
INSERT INTO documents (id, bank_id, original_text, content_hash, metadata, retain_params)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
ON CONFLICT (id, bank_id) DO UPDATE
|
ON CONFLICT (id, bank_id) DO UPDATE
|
||||||
SET original_text = EXCLUDED.original_text,
|
SET original_text = EXCLUDED.original_text,
|
||||||
content_hash = EXCLUDED.content_hash,
|
content_hash = EXCLUDED.content_hash,
|
||||||
metadata = EXCLUDED.metadata,
|
metadata = EXCLUDED.metadata,
|
||||||
|
retain_params = EXCLUDED.retain_params,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
""",
|
""",
|
||||||
document_id,
|
document_id,
|
||||||
bank_id,
|
bank_id,
|
||||||
combined_content,
|
combined_content,
|
||||||
content_hash,
|
content_hash,
|
||||||
json.dumps({}) # Empty metadata dict
|
json.dumps({}), # Empty metadata dict
|
||||||
|
json.dumps(retain_params) if retain_params else None
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -97,37 +97,56 @@ async def extract_entities_batch_optimized(
|
||||||
if all_entities_flat:
|
if all_entities_flat:
|
||||||
# [6.2.2] Batch resolve entities
|
# [6.2.2] Batch resolve entities
|
||||||
substep_6_2_2_start = time.time()
|
substep_6_2_2_start = time.time()
|
||||||
# Group by date for batch resolution (most will have same date)
|
# Group by date for batch resolution (round to hour to reduce buckets)
|
||||||
entities_by_date = {}
|
entities_by_date = {}
|
||||||
for idx, (unit_id, local_idx, fact_date) in enumerate(entity_to_unit):
|
for idx, (unit_id, local_idx, fact_date) in enumerate(entity_to_unit):
|
||||||
date_key = fact_date
|
# Round to hour to group facts from same time period
|
||||||
|
date_key = fact_date.replace(minute=0, second=0, microsecond=0)
|
||||||
if date_key not in entities_by_date:
|
if date_key not in entities_by_date:
|
||||||
entities_by_date[date_key] = []
|
entities_by_date[date_key] = []
|
||||||
entities_by_date[date_key].append((idx, all_entities_flat[idx]))
|
entities_by_date[date_key].append((idx, all_entities_flat[idx]))
|
||||||
|
|
||||||
_log(log_buffer, f" [6.2.2] Grouped into {len(entities_by_date)} date buckets, resolving...")
|
_log(log_buffer, f" [6.2.2] Grouped into {len(entities_by_date)} date buckets, resolving in parallel...")
|
||||||
|
|
||||||
# Resolve each date group in batch
|
# Resolve all date groups in PARALLEL using asyncio.gather
|
||||||
resolved_entity_ids = [None] * len(all_entities_flat)
|
resolved_entity_ids = [None] * len(all_entities_flat)
|
||||||
for date_idx, (fact_date, entities_group) in enumerate(entities_by_date.items(), 1):
|
|
||||||
|
# Prepare all resolution tasks
|
||||||
|
async def resolve_date_bucket(date_idx, date_key, entities_group):
|
||||||
date_bucket_start = time.time()
|
date_bucket_start = time.time()
|
||||||
indices = [idx for idx, _ in entities_group]
|
indices = [idx for idx, _ in entities_group]
|
||||||
entities_data = [entity_data for _, entity_data in entities_group]
|
entities_data = [entity_data for _, entity_data in entities_group]
|
||||||
|
# Use the first fact's date for this bucket (all should be in same hour)
|
||||||
|
fact_date = entity_to_unit[indices[0]][2]
|
||||||
|
|
||||||
|
# Pass conn=None to let each parallel task acquire its own connection
|
||||||
batch_resolved = await entity_resolver.resolve_entities_batch(
|
batch_resolved = await entity_resolver.resolve_entities_batch(
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
entities_data=entities_data,
|
entities_data=entities_data,
|
||||||
context=context,
|
context=context,
|
||||||
unit_event_date=fact_date,
|
unit_event_date=fact_date,
|
||||||
conn=conn
|
conn=None # Each task gets its own connection from pool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if len(entities_by_date) <= 10: # Only log individual buckets if there aren't too many
|
||||||
|
_log(log_buffer, f" [6.2.2.{date_idx}] Resolved {len(entities_data)} entities in {time.time() - date_bucket_start:.3f}s")
|
||||||
|
|
||||||
|
return indices, batch_resolved
|
||||||
|
|
||||||
|
# Execute all resolution tasks in parallel
|
||||||
|
import asyncio
|
||||||
|
tasks = [
|
||||||
|
resolve_date_bucket(date_idx, date_key, entities_group)
|
||||||
|
for date_idx, (date_key, entities_group) in enumerate(entities_by_date.items(), 1)
|
||||||
|
]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
# Map results back to resolved_entity_ids
|
||||||
|
for indices, batch_resolved in results:
|
||||||
for idx, entity_id in zip(indices, batch_resolved):
|
for idx, entity_id in zip(indices, batch_resolved):
|
||||||
resolved_entity_ids[idx] = entity_id
|
resolved_entity_ids[idx] = entity_id
|
||||||
|
|
||||||
_log(log_buffer, f" [6.2.2.{date_idx}] Resolved {len(entities_data)} entities in {time.time() - date_bucket_start:.3f}s")
|
_log(log_buffer, f" [6.2.2] Resolve entities: {len(all_entities_flat)} entities across {len(entities_by_date)} buckets in {time.time() - substep_6_2_2_start:.3f}s")
|
||||||
|
|
||||||
_log(log_buffer, f" [6.2.2] Resolve entities: {len(all_entities_flat)} entities in {time.time() - substep_6_2_2_start:.3f}s")
|
|
||||||
|
|
||||||
# [6.2.3] Create unit-entity links in BATCH
|
# [6.2.3] Create unit-entity links in BATCH
|
||||||
substep_6_2_3_start = time.time()
|
substep_6_2_3_start = time.time()
|
||||||
|
|
@ -444,7 +463,6 @@ async def insert_entity_links_batch(conn, links: List[tuple]):
|
||||||
if not links:
|
if not links:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
|
||||||
await conn.executemany(
|
await conn.executemany(
|
||||||
"""
|
"""
|
||||||
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
|
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
|
||||||
|
|
@ -453,8 +471,6 @@ async def insert_entity_links_batch(conn, links: List[tuple]):
|
||||||
""",
|
""",
|
||||||
links
|
links
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to insert entity links: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
async def create_causal_links_batch(
|
async def create_causal_links_batch(
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ async def retain_batch(
|
||||||
task_backend,
|
task_backend,
|
||||||
format_date_fn,
|
format_date_fn,
|
||||||
duplicate_checker_fn,
|
duplicate_checker_fn,
|
||||||
|
regenerate_observations_fn,
|
||||||
bank_id: str,
|
bank_id: str,
|
||||||
contents_dicts: List[Dict[str, Any]],
|
contents_dicts: List[Dict[str, Any]],
|
||||||
document_id: Optional[str] = None,
|
document_id: Optional[str] = None,
|
||||||
|
|
@ -57,6 +58,7 @@ async def retain_batch(
|
||||||
task_backend: Task backend for background jobs
|
task_backend: Task backend for background jobs
|
||||||
format_date_fn: Function to format datetime to readable string
|
format_date_fn: Function to format datetime to readable string
|
||||||
duplicate_checker_fn: Function to check for duplicate facts
|
duplicate_checker_fn: Function to check for duplicate facts
|
||||||
|
regenerate_observations_fn: Async function to regenerate observations for entities
|
||||||
bank_id: Bank identifier
|
bank_id: Bank identifier
|
||||||
contents_dicts: List of content dictionaries
|
contents_dicts: List of content dictionaries
|
||||||
document_id: Optional document ID
|
document_id: Optional document ID
|
||||||
|
|
@ -102,7 +104,7 @@ async def retain_batch(
|
||||||
agent_name,
|
agent_name,
|
||||||
extract_opinions
|
extract_opinions
|
||||||
)
|
)
|
||||||
log_buffer.append(f"[1] Extract facts: {len(extracted_facts)} facts from {len(contents)} contents in {time.time() - step_start:.3f}s")
|
log_buffer.append(f"[1] Extract facts: {len(extracted_facts)} facts, {len(chunks)} chunks from {len(contents)} contents in {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
if not extracted_facts:
|
if not extracted_facts:
|
||||||
return [[] for _ in contents]
|
return [[] for _ in contents]
|
||||||
|
|
@ -124,39 +126,140 @@ async def retain_batch(
|
||||||
for extracted_fact, embedding in zip(extracted_facts, embeddings)
|
for extracted_fact, embedding in zip(extracted_facts, embeddings)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Track document IDs for logging
|
||||||
|
document_ids_added = []
|
||||||
|
|
||||||
|
# Group contents by document_id for document tracking and chunk storage
|
||||||
|
from collections import defaultdict
|
||||||
|
contents_by_doc = defaultdict(list)
|
||||||
|
for idx, content_dict in enumerate(contents_dicts):
|
||||||
|
doc_id = content_dict.get("document_id")
|
||||||
|
contents_by_doc[doc_id].append((idx, content_dict))
|
||||||
|
|
||||||
# Step 4: Database transaction
|
# Step 4: Database transaction
|
||||||
async with acquire_with_retry(pool) as conn:
|
async with acquire_with_retry(pool) as conn:
|
||||||
async with conn.transaction():
|
async with conn.transaction():
|
||||||
# Ensure bank exists
|
# Ensure bank exists
|
||||||
await fact_storage.ensure_bank_exists(conn, bank_id)
|
await fact_storage.ensure_bank_exists(conn, bank_id)
|
||||||
|
|
||||||
# Handle document tracking
|
# Handle document tracking for all documents
|
||||||
if document_id:
|
|
||||||
combined_content = "\n".join([c.get("content", "") for c in contents_dicts])
|
|
||||||
await fact_storage.handle_document_tracking(
|
|
||||||
conn, bank_id, document_id, combined_content, is_first_batch
|
|
||||||
)
|
|
||||||
elif chunks:
|
|
||||||
# Generate document_id for chunk storage
|
|
||||||
document_id = str(uuid.uuid4())
|
|
||||||
combined_content = "\n".join([c.get("content", "") for c in contents_dicts])
|
|
||||||
await fact_storage.handle_document_tracking(
|
|
||||||
conn, bank_id, document_id, combined_content, is_first_batch
|
|
||||||
)
|
|
||||||
log_buffer.append(f"[2.5] Generated document_id: {document_id}")
|
|
||||||
|
|
||||||
# Store chunks and map to facts
|
|
||||||
step_start = time.time()
|
step_start = time.time()
|
||||||
chunk_id_map = {}
|
# Map None document_id to generated UUIDs
|
||||||
if document_id and chunks:
|
doc_id_mapping = {} # Maps original doc_id (including None) to actual doc_id used
|
||||||
chunk_id_map = await chunk_storage.store_chunks_batch(conn, bank_id, document_id, chunks)
|
|
||||||
log_buffer.append(f"[3] Store chunks: {len(chunks)} chunks in {time.time() - step_start:.3f}s")
|
|
||||||
|
|
||||||
# Map chunk_ids to facts
|
if document_id:
|
||||||
facts_chunk_indices = [fact.chunk_index for fact in extracted_facts]
|
# Legacy: single document_id parameter
|
||||||
chunk_ids = chunk_storage.map_facts_to_chunks(facts_chunk_indices, chunk_id_map)
|
combined_content = "\n".join([c.get("content", "") for c in contents_dicts])
|
||||||
for processed_fact, chunk_id in zip(processed_facts, chunk_ids):
|
retain_params = {}
|
||||||
|
if contents_dicts:
|
||||||
|
first_item = contents_dicts[0]
|
||||||
|
if first_item.get("context"):
|
||||||
|
retain_params["context"] = first_item["context"]
|
||||||
|
if first_item.get("event_date"):
|
||||||
|
retain_params["event_date"] = first_item["event_date"].isoformat() if hasattr(first_item["event_date"], "isoformat") else str(first_item["event_date"])
|
||||||
|
if first_item.get("metadata"):
|
||||||
|
retain_params["metadata"] = first_item["metadata"]
|
||||||
|
|
||||||
|
await fact_storage.handle_document_tracking(
|
||||||
|
conn, bank_id, document_id, combined_content, is_first_batch, retain_params
|
||||||
|
)
|
||||||
|
document_ids_added.append(document_id)
|
||||||
|
doc_id_mapping[None] = document_id # For backwards compatibility
|
||||||
|
else:
|
||||||
|
# Handle per-item document_ids (create documents if any item has document_id or if chunks exist)
|
||||||
|
has_any_doc_ids = any(item.get("document_id") for item in contents_dicts)
|
||||||
|
|
||||||
|
if has_any_doc_ids or chunks:
|
||||||
|
for original_doc_id, doc_contents in contents_by_doc.items():
|
||||||
|
actual_doc_id = original_doc_id
|
||||||
|
|
||||||
|
# Only create document record if:
|
||||||
|
# 1. Item has explicit document_id, OR
|
||||||
|
# 2. There are chunks (need document for chunk storage)
|
||||||
|
should_create_doc = (original_doc_id is not None) or chunks
|
||||||
|
|
||||||
|
if should_create_doc:
|
||||||
|
if actual_doc_id is None:
|
||||||
|
# No document_id but have chunks - generate one
|
||||||
|
actual_doc_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Store mapping for later use
|
||||||
|
doc_id_mapping[original_doc_id] = actual_doc_id
|
||||||
|
|
||||||
|
# Combine content for this document
|
||||||
|
combined_content = "\n".join([c.get("content", "") for _, c in doc_contents])
|
||||||
|
|
||||||
|
# Extract retain params from first content item
|
||||||
|
retain_params = {}
|
||||||
|
if doc_contents:
|
||||||
|
first_item = doc_contents[0][1]
|
||||||
|
if first_item.get("context"):
|
||||||
|
retain_params["context"] = first_item["context"]
|
||||||
|
if first_item.get("event_date"):
|
||||||
|
retain_params["event_date"] = first_item["event_date"].isoformat() if hasattr(first_item["event_date"], "isoformat") else str(first_item["event_date"])
|
||||||
|
if first_item.get("metadata"):
|
||||||
|
retain_params["metadata"] = first_item["metadata"]
|
||||||
|
|
||||||
|
await fact_storage.handle_document_tracking(
|
||||||
|
conn, bank_id, actual_doc_id, combined_content, is_first_batch, retain_params
|
||||||
|
)
|
||||||
|
document_ids_added.append(actual_doc_id)
|
||||||
|
|
||||||
|
if document_ids_added:
|
||||||
|
log_buffer.append(f"[2.5] Document tracking: {len(document_ids_added)} documents in {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
|
# Store chunks and map to facts for all documents
|
||||||
|
step_start = time.time()
|
||||||
|
chunk_id_map_by_doc = {} # Maps (doc_id, chunk_index) -> chunk_id
|
||||||
|
|
||||||
|
if chunks:
|
||||||
|
# Group chunks by their source document
|
||||||
|
chunks_by_doc = defaultdict(list)
|
||||||
|
for chunk in chunks:
|
||||||
|
# chunk.content_index tells us which content this chunk came from
|
||||||
|
original_doc_id = contents_dicts[chunk.content_index].get("document_id")
|
||||||
|
# Map to actual document_id (handles None -> generated UUID mapping)
|
||||||
|
actual_doc_id = doc_id_mapping.get(original_doc_id, original_doc_id)
|
||||||
|
if actual_doc_id is None and document_id:
|
||||||
|
actual_doc_id = document_id
|
||||||
|
chunks_by_doc[actual_doc_id].append(chunk)
|
||||||
|
|
||||||
|
# Store chunks for each document
|
||||||
|
for doc_id, doc_chunks in chunks_by_doc.items():
|
||||||
|
chunk_id_map = await chunk_storage.store_chunks_batch(conn, bank_id, doc_id, doc_chunks)
|
||||||
|
# Store mapping with document context
|
||||||
|
for chunk_idx, chunk_id in chunk_id_map.items():
|
||||||
|
chunk_id_map_by_doc[(doc_id, chunk_idx)] = chunk_id
|
||||||
|
|
||||||
|
log_buffer.append(f"[3] Store chunks: {len(chunks)} chunks for {len(chunks_by_doc)} documents in {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
|
# Map chunk_ids and document_ids to facts
|
||||||
|
for fact, processed_fact in zip(extracted_facts, processed_facts):
|
||||||
|
# Get the original document_id for this fact's source content
|
||||||
|
original_doc_id = contents_dicts[fact.content_index].get("document_id")
|
||||||
|
# Map to actual document_id (handles None -> generated UUID mapping)
|
||||||
|
actual_doc_id = doc_id_mapping.get(original_doc_id, original_doc_id)
|
||||||
|
if actual_doc_id is None and document_id:
|
||||||
|
actual_doc_id = document_id
|
||||||
|
|
||||||
|
# Set document_id on the fact
|
||||||
|
processed_fact.document_id = actual_doc_id
|
||||||
|
|
||||||
|
# Map chunk_id if this fact came from a chunk
|
||||||
|
if fact.chunk_index is not None:
|
||||||
|
# Look up chunk_id using (doc_id, chunk_index)
|
||||||
|
chunk_id = chunk_id_map_by_doc.get((actual_doc_id, fact.chunk_index))
|
||||||
|
if chunk_id:
|
||||||
processed_fact.chunk_id = chunk_id
|
processed_fact.chunk_id = chunk_id
|
||||||
|
else:
|
||||||
|
# No chunks - still need to set document_id on facts
|
||||||
|
for fact, processed_fact in zip(extracted_facts, processed_facts):
|
||||||
|
original_doc_id = contents_dicts[fact.content_index].get("document_id")
|
||||||
|
# Map to actual document_id (handles None -> generated UUID mapping)
|
||||||
|
actual_doc_id = doc_id_mapping.get(original_doc_id, original_doc_id)
|
||||||
|
if actual_doc_id is None and document_id:
|
||||||
|
actual_doc_id = document_id
|
||||||
|
processed_fact.document_id = actual_doc_id
|
||||||
|
|
||||||
# Deduplication
|
# Deduplication
|
||||||
step_start = time.time()
|
step_start = time.time()
|
||||||
|
|
@ -171,15 +274,15 @@ async def retain_batch(
|
||||||
if not non_duplicate_facts:
|
if not non_duplicate_facts:
|
||||||
return [[] for _ in contents]
|
return [[] for _ in contents]
|
||||||
|
|
||||||
# Insert facts
|
# Insert facts (document_id is now stored per-fact)
|
||||||
step_start = time.time()
|
step_start = time.time()
|
||||||
unit_ids = await fact_storage.insert_facts_batch(conn, bank_id, non_duplicate_facts, document_id)
|
unit_ids = await fact_storage.insert_facts_batch(conn, bank_id, non_duplicate_facts)
|
||||||
log_buffer.append(f"[5] Insert facts: {len(unit_ids)} units in {time.time() - step_start:.3f}s")
|
log_buffer.append(f"[5] Insert facts: {len(unit_ids)} units in {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
# Process entities
|
# Process entities
|
||||||
step_start = time.time()
|
step_start = time.time()
|
||||||
entity_links = await entity_processing.process_entities_batch(
|
entity_links = await entity_processing.process_entities_batch(
|
||||||
entity_resolver, conn, bank_id, unit_ids, non_duplicate_facts
|
entity_resolver, conn, bank_id, unit_ids, non_duplicate_facts, log_buffer
|
||||||
)
|
)
|
||||||
log_buffer.append(f"[6] Process entities: {len(entity_links)} links in {time.time() - step_start:.3f}s")
|
log_buffer.append(f"[6] Process entities: {len(entity_links)} links in {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
|
|
@ -213,13 +316,16 @@ async def retain_batch(
|
||||||
total_time = time.time() - start_time
|
total_time = time.time() - start_time
|
||||||
log_buffer.append(f"{'='*60}")
|
log_buffer.append(f"{'='*60}")
|
||||||
log_buffer.append(f"RETAIN_BATCH COMPLETE: {len(unit_ids)} units in {total_time:.3f}s")
|
log_buffer.append(f"RETAIN_BATCH COMPLETE: {len(unit_ids)} units in {total_time:.3f}s")
|
||||||
|
if document_ids_added:
|
||||||
|
log_buffer.append(f"Documents: {', '.join(document_ids_added)}")
|
||||||
log_buffer.append(f"{'='*60}")
|
log_buffer.append(f"{'='*60}")
|
||||||
|
|
||||||
logger.info("\n" + "\n".join(log_buffer) + "\n")
|
logger.info("\n" + "\n".join(log_buffer) + "\n")
|
||||||
|
|
||||||
# Trigger background tasks
|
# Trigger background tasks AFTER transaction commits
|
||||||
await _trigger_background_tasks(
|
await _trigger_background_tasks(
|
||||||
task_backend,
|
task_backend,
|
||||||
|
regenerate_observations_fn,
|
||||||
bank_id,
|
bank_id,
|
||||||
unit_ids,
|
unit_ids,
|
||||||
non_duplicate_facts,
|
non_duplicate_facts,
|
||||||
|
|
@ -261,12 +367,13 @@ def _map_results_to_contents(
|
||||||
|
|
||||||
async def _trigger_background_tasks(
|
async def _trigger_background_tasks(
|
||||||
task_backend,
|
task_backend,
|
||||||
|
regenerate_observations_fn,
|
||||||
bank_id: str,
|
bank_id: str,
|
||||||
unit_ids: List[str],
|
unit_ids: List[str],
|
||||||
facts: List[ProcessedFact],
|
facts: List[ProcessedFact],
|
||||||
entity_links: List
|
entity_links: List
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Trigger opinion reinforcement and observation regeneration tasks."""
|
"""Trigger opinion reinforcement and observation regeneration (sync)."""
|
||||||
# Trigger opinion reinforcement if there are entities
|
# Trigger opinion reinforcement if there are entities
|
||||||
fact_entities = [[e.name for e in fact.entities] for fact in facts]
|
fact_entities = [[e.name for e in fact.entities] for fact in facts]
|
||||||
if any(fact_entities):
|
if any(fact_entities):
|
||||||
|
|
@ -278,11 +385,11 @@ async def _trigger_background_tasks(
|
||||||
'unit_entities': fact_entities
|
'unit_entities': fact_entities
|
||||||
})
|
})
|
||||||
|
|
||||||
# Trigger observation regeneration for top entities
|
# Regenerate observations synchronously for top entities
|
||||||
TOP_N_ENTITIES = 5
|
TOP_N_ENTITIES = 5
|
||||||
MIN_FACTS_THRESHOLD = 5
|
MIN_FACTS_THRESHOLD = 5
|
||||||
|
|
||||||
if entity_links:
|
if entity_links and regenerate_observations_fn:
|
||||||
unique_entity_ids = set()
|
unique_entity_ids = set()
|
||||||
for link in entity_links:
|
for link in entity_links:
|
||||||
# links are tuples: (unit_id, entity_id, confidence)
|
# links are tuples: (unit_id, entity_id, confidence)
|
||||||
|
|
@ -290,9 +397,9 @@ async def _trigger_background_tasks(
|
||||||
unique_entity_ids.add(str(link[1]))
|
unique_entity_ids.add(str(link[1]))
|
||||||
|
|
||||||
if unique_entity_ids:
|
if unique_entity_ids:
|
||||||
await task_backend.submit_task({
|
# Run observation regeneration synchronously
|
||||||
'type': 'regenerate_observations',
|
await regenerate_observations_fn(
|
||||||
'bank_id': bank_id,
|
bank_id=bank_id,
|
||||||
'entity_ids': list(unique_entity_ids)[:TOP_N_ENTITIES],
|
entity_ids=list(unique_entity_ids)[:TOP_N_ENTITIES],
|
||||||
'min_facts': MIN_FACTS_THRESHOLD
|
min_facts=MIN_FACTS_THRESHOLD
|
||||||
})
|
)
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ class ExtractedFact:
|
||||||
entities: List[str] = field(default_factory=list)
|
entities: List[str] = field(default_factory=list)
|
||||||
occurred_start: Optional[datetime] = None
|
occurred_start: Optional[datetime] = None
|
||||||
occurred_end: Optional[datetime] = None
|
occurred_end: Optional[datetime] = None
|
||||||
|
where: Optional[str] = None # WHERE the fact occurred or is about
|
||||||
causal_relations: List[CausalRelation] = field(default_factory=list)
|
causal_relations: List[CausalRelation] = field(default_factory=list)
|
||||||
|
|
||||||
# Context from the content item
|
# Context from the content item
|
||||||
|
|
@ -110,6 +111,9 @@ class ProcessedFact:
|
||||||
context: str
|
context: str
|
||||||
metadata: Dict[str, str]
|
metadata: Dict[str, str]
|
||||||
|
|
||||||
|
# Location data
|
||||||
|
where: Optional[str] = None
|
||||||
|
|
||||||
# Entities
|
# Entities
|
||||||
entities: List[EntityRef] = field(default_factory=list)
|
entities: List[EntityRef] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
@ -119,6 +123,9 @@ class ProcessedFact:
|
||||||
# Chunk reference
|
# Chunk reference
|
||||||
chunk_id: Optional[str] = None
|
chunk_id: Optional[str] = None
|
||||||
|
|
||||||
|
# Document reference (denormalized for query performance)
|
||||||
|
document_id: Optional[str] = None
|
||||||
|
|
||||||
# DB fields (set after insertion)
|
# DB fields (set after insertion)
|
||||||
unit_id: Optional[UUID] = None
|
unit_id: Optional[UUID] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class NodeVisit(BaseModel):
|
||||||
node_id: str = Field(description="Memory unit ID")
|
node_id: str = Field(description="Memory unit ID")
|
||||||
text: str = Field(description="Memory unit text content")
|
text: str = Field(description="Memory unit text content")
|
||||||
context: str = Field(description="Memory unit context")
|
context: str = Field(description="Memory unit context")
|
||||||
event_date: datetime = Field(description="When the memory occurred")
|
event_date: Optional[datetime] = Field(default=None, description="When the memory occurred")
|
||||||
access_count: int = Field(description="Number of times accessed before this search")
|
access_count: int = Field(description="Number of times accessed before this search")
|
||||||
|
|
||||||
# How this node was reached
|
# How this node was reached
|
||||||
|
|
@ -100,6 +100,7 @@ class RetrievalResult(BaseModel):
|
||||||
text: str = Field(description="Memory unit text content")
|
text: str = Field(description="Memory unit text content")
|
||||||
context: str = Field(default="", description="Memory unit context")
|
context: str = Field(default="", description="Memory unit context")
|
||||||
event_date: Optional[datetime] = Field(default=None, description="When the memory occurred")
|
event_date: Optional[datetime] = Field(default=None, description="When the memory occurred")
|
||||||
|
fact_type: Optional[str] = Field(default=None, description="Fact type (world, bank, opinion)")
|
||||||
score: float = Field(description="Score from this retrieval method")
|
score: float = Field(description="Score from this retrieval method")
|
||||||
score_name: str = Field(description="Name of the score (e.g., 'similarity', 'bm25_score', 'activation')")
|
score_name: str = Field(description="Name of the score (e.g., 'similarity', 'bm25_score', 'activation')")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,9 @@ class SearchTracer:
|
||||||
"""
|
"""
|
||||||
retrieval_results = []
|
retrieval_results = []
|
||||||
for rank, (doc_id, data) in enumerate(results, start=1):
|
for rank, (doc_id, data) in enumerate(results, start=1):
|
||||||
score = data.get(score_field, 0.0)
|
score = data.get(score_field)
|
||||||
|
if score is None:
|
||||||
|
score = 0.0
|
||||||
retrieval_results.append(
|
retrieval_results.append(
|
||||||
RetrievalResult(
|
RetrievalResult(
|
||||||
rank=rank,
|
rank=rank,
|
||||||
|
|
@ -311,6 +313,7 @@ class SearchTracer:
|
||||||
text=data.get("text", ""),
|
text=data.get("text", ""),
|
||||||
context=data.get("context", ""),
|
context=data.get("context", ""),
|
||||||
event_date=data.get("event_date"),
|
event_date=data.get("event_date"),
|
||||||
|
fact_type=data.get("fact_type"),
|
||||||
score=score,
|
score=score,
|
||||||
score_name=score_field,
|
score_name=score_field,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ def get_platform_binary_name() -> str:
|
||||||
|
|
||||||
Supported platforms:
|
Supported platforms:
|
||||||
- macOS ARM64 (darwin-aarch64)
|
- macOS ARM64 (darwin-aarch64)
|
||||||
- Linux x86_64
|
- Linux x86_64 (gnu)
|
||||||
|
- Linux ARM64 (gnu)
|
||||||
- Windows x86_64
|
- Windows x86_64
|
||||||
"""
|
"""
|
||||||
system = platform.system().lower()
|
system = platform.system().lower()
|
||||||
|
|
@ -42,19 +43,21 @@ def get_platform_binary_name() -> str:
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Embedded PostgreSQL is not supported on architecture: {machine}. "
|
f"Embedded PostgreSQL is not supported on architecture: {machine}. "
|
||||||
f"Supported architectures: x86_64/amd64 (Linux, Windows), aarch64/arm64 (macOS)"
|
f"Supported architectures: x86_64/amd64 (Linux, Windows), aarch64/arm64 (macOS, Linux)"
|
||||||
)
|
)
|
||||||
|
|
||||||
if system == "darwin" and arch == "aarch64":
|
if system == "darwin" and arch == "aarch64":
|
||||||
return "pg0-darwin-aarch64"
|
return "pg0-darwin-aarch64"
|
||||||
elif system == "linux" and arch == "x86_64":
|
elif system == "linux" and arch == "x86_64":
|
||||||
return "pg0-linux-x86_64"
|
return "pg0-linux-x86_64-gnu"
|
||||||
|
elif system == "linux" and arch == "aarch64":
|
||||||
|
return "pg0-linux-aarch64-gnu"
|
||||||
elif system == "windows" and arch == "x86_64":
|
elif system == "windows" and arch == "x86_64":
|
||||||
return "pg0-windows-x86_64.exe"
|
return "pg0-windows-x86_64.exe"
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Embedded PostgreSQL is not supported on {system}-{arch}. "
|
f"Embedded PostgreSQL is not supported on {system}-{arch}. "
|
||||||
f"Supported platforms: darwin-aarch64 (macOS ARM), linux-x86_64, windows-x86_64"
|
f"Supported platforms: darwin-aarch64 (macOS ARM), linux-x86_64-gnu, linux-aarch64-gnu, windows-x86_64"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ produces semantically correct and complete facts.
|
||||||
import pytest
|
import pytest
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from hindsight_api.engine.fact_extraction import extract_facts_from_text
|
from hindsight_api.engine.retain.fact_extraction import extract_facts_from_text
|
||||||
from hindsight_api import LLMConfig
|
from hindsight_api import LLMConfig
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ hindsight_client_api/docs/BankProfileResponse.md
|
||||||
hindsight_client_api/docs/Budget.md
|
hindsight_client_api/docs/Budget.md
|
||||||
hindsight_client_api/docs/ChunkData.md
|
hindsight_client_api/docs/ChunkData.md
|
||||||
hindsight_client_api/docs/ChunkIncludeOptions.md
|
hindsight_client_api/docs/ChunkIncludeOptions.md
|
||||||
|
hindsight_client_api/docs/ChunkResponse.md
|
||||||
hindsight_client_api/docs/CreateBankRequest.md
|
hindsight_client_api/docs/CreateBankRequest.md
|
||||||
hindsight_client_api/docs/DefaultApi.md
|
hindsight_client_api/docs/DefaultApi.md
|
||||||
hindsight_client_api/docs/DeleteResponse.md
|
hindsight_client_api/docs/DeleteResponse.md
|
||||||
|
|
@ -54,6 +55,7 @@ hindsight_client_api/models/bank_profile_response.py
|
||||||
hindsight_client_api/models/budget.py
|
hindsight_client_api/models/budget.py
|
||||||
hindsight_client_api/models/chunk_data.py
|
hindsight_client_api/models/chunk_data.py
|
||||||
hindsight_client_api/models/chunk_include_options.py
|
hindsight_client_api/models/chunk_include_options.py
|
||||||
|
hindsight_client_api/models/chunk_response.py
|
||||||
hindsight_client_api/models/create_bank_request.py
|
hindsight_client_api/models/create_bank_request.py
|
||||||
hindsight_client_api/models/delete_response.py
|
hindsight_client_api/models/delete_response.py
|
||||||
hindsight_client_api/models/document_response.py
|
hindsight_client_api/models/document_response.py
|
||||||
|
|
@ -93,6 +95,7 @@ hindsight_client_api/test/test_bank_profile_response.py
|
||||||
hindsight_client_api/test/test_budget.py
|
hindsight_client_api/test/test_budget.py
|
||||||
hindsight_client_api/test/test_chunk_data.py
|
hindsight_client_api/test/test_chunk_data.py
|
||||||
hindsight_client_api/test/test_chunk_include_options.py
|
hindsight_client_api/test/test_chunk_include_options.py
|
||||||
|
hindsight_client_api/test/test_chunk_response.py
|
||||||
hindsight_client_api/test/test_create_bank_request.py
|
hindsight_client_api/test/test_create_bank_request.py
|
||||||
hindsight_client_api/test/test_default_api.py
|
hindsight_client_api/test/test_default_api.py
|
||||||
hindsight_client_api/test/test_delete_response.py
|
hindsight_client_api/test/test_delete_response.py
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ __all__ = [
|
||||||
"Budget",
|
"Budget",
|
||||||
"ChunkData",
|
"ChunkData",
|
||||||
"ChunkIncludeOptions",
|
"ChunkIncludeOptions",
|
||||||
|
"ChunkResponse",
|
||||||
"CreateBankRequest",
|
"CreateBankRequest",
|
||||||
"DeleteResponse",
|
"DeleteResponse",
|
||||||
"DocumentResponse",
|
"DocumentResponse",
|
||||||
|
|
@ -92,6 +93,7 @@ from hindsight_client_api.models.bank_profile_response import BankProfileRespons
|
||||||
from hindsight_client_api.models.budget import Budget as Budget
|
from hindsight_client_api.models.budget import Budget as Budget
|
||||||
from hindsight_client_api.models.chunk_data import ChunkData as ChunkData
|
from hindsight_client_api.models.chunk_data import ChunkData as ChunkData
|
||||||
from hindsight_client_api.models.chunk_include_options import ChunkIncludeOptions as ChunkIncludeOptions
|
from hindsight_client_api.models.chunk_include_options import ChunkIncludeOptions as ChunkIncludeOptions
|
||||||
|
from hindsight_client_api.models.chunk_response import ChunkResponse as ChunkResponse
|
||||||
from hindsight_client_api.models.create_bank_request import CreateBankRequest as CreateBankRequest
|
from hindsight_client_api.models.create_bank_request import CreateBankRequest as CreateBankRequest
|
||||||
from hindsight_client_api.models.delete_response import DeleteResponse as DeleteResponse
|
from hindsight_client_api.models.delete_response import DeleteResponse as DeleteResponse
|
||||||
from hindsight_client_api.models.document_response import DocumentResponse as DocumentResponse
|
from hindsight_client_api.models.document_response import DocumentResponse as DocumentResponse
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from hindsight_client_api.models.add_background_request import AddBackgroundRequ
|
||||||
from hindsight_client_api.models.background_response import BackgroundResponse
|
from hindsight_client_api.models.background_response import BackgroundResponse
|
||||||
from hindsight_client_api.models.bank_list_response import BankListResponse
|
from hindsight_client_api.models.bank_list_response import BankListResponse
|
||||||
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
|
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
|
||||||
|
from hindsight_client_api.models.chunk_response import ChunkResponse
|
||||||
from hindsight_client_api.models.create_bank_request import CreateBankRequest
|
from hindsight_client_api.models.create_bank_request import CreateBankRequest
|
||||||
from hindsight_client_api.models.delete_response import DeleteResponse
|
from hindsight_client_api.models.delete_response import DeleteResponse
|
||||||
from hindsight_client_api.models.document_response import DocumentResponse
|
from hindsight_client_api.models.document_response import DocumentResponse
|
||||||
|
|
@ -2001,6 +2002,269 @@ class DefaultApi:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@validate_call
|
||||||
|
async def get_chunk(
|
||||||
|
self,
|
||||||
|
chunk_id: StrictStr,
|
||||||
|
_request_timeout: Union[
|
||||||
|
None,
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Tuple[
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Annotated[StrictFloat, Field(gt=0)]
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
_request_auth: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_content_type: Optional[StrictStr] = None,
|
||||||
|
_headers: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
|
||||||
|
) -> ChunkResponse:
|
||||||
|
"""Get chunk details
|
||||||
|
|
||||||
|
Get a specific chunk by its ID
|
||||||
|
|
||||||
|
:param chunk_id: (required)
|
||||||
|
:type chunk_id: str
|
||||||
|
:param _request_timeout: timeout setting for this request. If one
|
||||||
|
number provided, it will be total request
|
||||||
|
timeout. It can also be a pair (tuple) of
|
||||||
|
(connection, read) timeouts.
|
||||||
|
:type _request_timeout: int, tuple(int, int), optional
|
||||||
|
:param _request_auth: set to override the auth_settings for an a single
|
||||||
|
request; this effectively ignores the
|
||||||
|
authentication in the spec for a single request.
|
||||||
|
:type _request_auth: dict, optional
|
||||||
|
:param _content_type: force content-type for the request.
|
||||||
|
:type _content_type: str, Optional
|
||||||
|
:param _headers: set to override the headers for a single
|
||||||
|
request; this effectively ignores the headers
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _headers: dict, optional
|
||||||
|
:param _host_index: set to override the host_index for a single
|
||||||
|
request; this effectively ignores the host_index
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _host_index: int, optional
|
||||||
|
:return: Returns the result object.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
_param = self._get_chunk_serialize(
|
||||||
|
chunk_id=chunk_id,
|
||||||
|
_request_auth=_request_auth,
|
||||||
|
_content_type=_content_type,
|
||||||
|
_headers=_headers,
|
||||||
|
_host_index=_host_index
|
||||||
|
)
|
||||||
|
|
||||||
|
_response_types_map: Dict[str, Optional[str]] = {
|
||||||
|
'200': "ChunkResponse",
|
||||||
|
'422': "HTTPValidationError",
|
||||||
|
}
|
||||||
|
response_data = await self.api_client.call_api(
|
||||||
|
*_param,
|
||||||
|
_request_timeout=_request_timeout
|
||||||
|
)
|
||||||
|
await response_data.read()
|
||||||
|
return self.api_client.response_deserialize(
|
||||||
|
response_data=response_data,
|
||||||
|
response_types_map=_response_types_map,
|
||||||
|
).data
|
||||||
|
|
||||||
|
|
||||||
|
@validate_call
|
||||||
|
async def get_chunk_with_http_info(
|
||||||
|
self,
|
||||||
|
chunk_id: StrictStr,
|
||||||
|
_request_timeout: Union[
|
||||||
|
None,
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Tuple[
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Annotated[StrictFloat, Field(gt=0)]
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
_request_auth: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_content_type: Optional[StrictStr] = None,
|
||||||
|
_headers: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
|
||||||
|
) -> ApiResponse[ChunkResponse]:
|
||||||
|
"""Get chunk details
|
||||||
|
|
||||||
|
Get a specific chunk by its ID
|
||||||
|
|
||||||
|
:param chunk_id: (required)
|
||||||
|
:type chunk_id: str
|
||||||
|
:param _request_timeout: timeout setting for this request. If one
|
||||||
|
number provided, it will be total request
|
||||||
|
timeout. It can also be a pair (tuple) of
|
||||||
|
(connection, read) timeouts.
|
||||||
|
:type _request_timeout: int, tuple(int, int), optional
|
||||||
|
:param _request_auth: set to override the auth_settings for an a single
|
||||||
|
request; this effectively ignores the
|
||||||
|
authentication in the spec for a single request.
|
||||||
|
:type _request_auth: dict, optional
|
||||||
|
:param _content_type: force content-type for the request.
|
||||||
|
:type _content_type: str, Optional
|
||||||
|
:param _headers: set to override the headers for a single
|
||||||
|
request; this effectively ignores the headers
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _headers: dict, optional
|
||||||
|
:param _host_index: set to override the host_index for a single
|
||||||
|
request; this effectively ignores the host_index
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _host_index: int, optional
|
||||||
|
:return: Returns the result object.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
_param = self._get_chunk_serialize(
|
||||||
|
chunk_id=chunk_id,
|
||||||
|
_request_auth=_request_auth,
|
||||||
|
_content_type=_content_type,
|
||||||
|
_headers=_headers,
|
||||||
|
_host_index=_host_index
|
||||||
|
)
|
||||||
|
|
||||||
|
_response_types_map: Dict[str, Optional[str]] = {
|
||||||
|
'200': "ChunkResponse",
|
||||||
|
'422': "HTTPValidationError",
|
||||||
|
}
|
||||||
|
response_data = await self.api_client.call_api(
|
||||||
|
*_param,
|
||||||
|
_request_timeout=_request_timeout
|
||||||
|
)
|
||||||
|
await response_data.read()
|
||||||
|
return self.api_client.response_deserialize(
|
||||||
|
response_data=response_data,
|
||||||
|
response_types_map=_response_types_map,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@validate_call
|
||||||
|
async def get_chunk_without_preload_content(
|
||||||
|
self,
|
||||||
|
chunk_id: StrictStr,
|
||||||
|
_request_timeout: Union[
|
||||||
|
None,
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Tuple[
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Annotated[StrictFloat, Field(gt=0)]
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
_request_auth: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_content_type: Optional[StrictStr] = None,
|
||||||
|
_headers: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
|
||||||
|
) -> RESTResponseType:
|
||||||
|
"""Get chunk details
|
||||||
|
|
||||||
|
Get a specific chunk by its ID
|
||||||
|
|
||||||
|
:param chunk_id: (required)
|
||||||
|
:type chunk_id: str
|
||||||
|
:param _request_timeout: timeout setting for this request. If one
|
||||||
|
number provided, it will be total request
|
||||||
|
timeout. It can also be a pair (tuple) of
|
||||||
|
(connection, read) timeouts.
|
||||||
|
:type _request_timeout: int, tuple(int, int), optional
|
||||||
|
:param _request_auth: set to override the auth_settings for an a single
|
||||||
|
request; this effectively ignores the
|
||||||
|
authentication in the spec for a single request.
|
||||||
|
:type _request_auth: dict, optional
|
||||||
|
:param _content_type: force content-type for the request.
|
||||||
|
:type _content_type: str, Optional
|
||||||
|
:param _headers: set to override the headers for a single
|
||||||
|
request; this effectively ignores the headers
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _headers: dict, optional
|
||||||
|
:param _host_index: set to override the host_index for a single
|
||||||
|
request; this effectively ignores the host_index
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _host_index: int, optional
|
||||||
|
:return: Returns the result object.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
_param = self._get_chunk_serialize(
|
||||||
|
chunk_id=chunk_id,
|
||||||
|
_request_auth=_request_auth,
|
||||||
|
_content_type=_content_type,
|
||||||
|
_headers=_headers,
|
||||||
|
_host_index=_host_index
|
||||||
|
)
|
||||||
|
|
||||||
|
_response_types_map: Dict[str, Optional[str]] = {
|
||||||
|
'200': "ChunkResponse",
|
||||||
|
'422': "HTTPValidationError",
|
||||||
|
}
|
||||||
|
response_data = await self.api_client.call_api(
|
||||||
|
*_param,
|
||||||
|
_request_timeout=_request_timeout
|
||||||
|
)
|
||||||
|
return response_data.response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_chunk_serialize(
|
||||||
|
self,
|
||||||
|
chunk_id,
|
||||||
|
_request_auth,
|
||||||
|
_content_type,
|
||||||
|
_headers,
|
||||||
|
_host_index,
|
||||||
|
) -> RequestSerialized:
|
||||||
|
|
||||||
|
_host = None
|
||||||
|
|
||||||
|
_collection_formats: Dict[str, str] = {
|
||||||
|
}
|
||||||
|
|
||||||
|
_path_params: Dict[str, str] = {}
|
||||||
|
_query_params: List[Tuple[str, str]] = []
|
||||||
|
_header_params: Dict[str, Optional[str]] = _headers or {}
|
||||||
|
_form_params: List[Tuple[str, str]] = []
|
||||||
|
_files: Dict[
|
||||||
|
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
|
||||||
|
] = {}
|
||||||
|
_body_params: Optional[bytes] = None
|
||||||
|
|
||||||
|
# process the path parameters
|
||||||
|
if chunk_id is not None:
|
||||||
|
_path_params['chunk_id'] = chunk_id
|
||||||
|
# process the query parameters
|
||||||
|
# process the header parameters
|
||||||
|
# process the form parameters
|
||||||
|
# process the body parameter
|
||||||
|
|
||||||
|
|
||||||
|
# set the HTTP header `Accept`
|
||||||
|
if 'Accept' not in _header_params:
|
||||||
|
_header_params['Accept'] = self.api_client.select_header_accept(
|
||||||
|
[
|
||||||
|
'application/json'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# authentication setting
|
||||||
|
_auth_settings: List[str] = [
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.api_client.param_serialize(
|
||||||
|
method='GET',
|
||||||
|
resource_path='/v1/default/chunks/{chunk_id}',
|
||||||
|
path_params=_path_params,
|
||||||
|
query_params=_query_params,
|
||||||
|
header_params=_header_params,
|
||||||
|
body=_body_params,
|
||||||
|
post_params=_form_params,
|
||||||
|
files=_files,
|
||||||
|
auth_settings=_auth_settings,
|
||||||
|
collection_formats=_collection_formats,
|
||||||
|
_host=_host,
|
||||||
|
_request_auth=_request_auth
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@validate_call
|
@validate_call
|
||||||
async def get_document(
|
async def get_document(
|
||||||
self,
|
self,
|
||||||
|
|
@ -5150,7 +5414,7 @@ class DefaultApi:
|
||||||
) -> RetainResponse:
|
) -> RetainResponse:
|
||||||
"""Retain memories
|
"""Retain memories
|
||||||
|
|
||||||
Retain memory items with automatic fact extraction. This is the main endpoint for storing memories. It supports both synchronous and asynchronous processing via the async parameter. Features: - Efficient batch processing - Automatic fact extraction from natural language - Entity recognition and linking - Document tracking with automatic upsert (when document_id is provided) - Temporal and semantic linking - Optional asynchronous processing The system automatically: 1. Extracts semantic facts from the content 2. Generates embeddings 3. Deduplicates similar facts 4. Creates temporal, semantic, and entity links 5. Tracks document metadata When async=true: - Returns immediately after queuing the task - Processing happens in the background - Use the operations endpoint to monitor progress When async=false (default): - Waits for processing to complete - Returns after all memories are stored Note: If document_id is provided and already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior).
|
Retain memory items with automatic fact extraction. This is the main endpoint for storing memories. It supports both synchronous and asynchronous processing via the async parameter. Features: - Efficient batch processing - Automatic fact extraction from natural language - Entity recognition and linking - Document tracking with automatic upsert (when document_id is provided on items) - Temporal and semantic linking - Optional asynchronous processing The system automatically: 1. Extracts semantic facts from the content 2. Generates embeddings 3. Deduplicates similar facts 4. Creates temporal, semantic, and entity links 5. Tracks document metadata When async=true: - Returns immediately after queuing the task - Processing happens in the background - Use the operations endpoint to monitor progress When async=false (default): - Waits for processing to complete - Returns after all memories are stored Note: If a memory item has a document_id that already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior). Items with the same document_id are grouped together for efficient processing.
|
||||||
|
|
||||||
:param bank_id: (required)
|
:param bank_id: (required)
|
||||||
:type bank_id: str
|
:type bank_id: str
|
||||||
|
|
@ -5222,7 +5486,7 @@ class DefaultApi:
|
||||||
) -> ApiResponse[RetainResponse]:
|
) -> ApiResponse[RetainResponse]:
|
||||||
"""Retain memories
|
"""Retain memories
|
||||||
|
|
||||||
Retain memory items with automatic fact extraction. This is the main endpoint for storing memories. It supports both synchronous and asynchronous processing via the async parameter. Features: - Efficient batch processing - Automatic fact extraction from natural language - Entity recognition and linking - Document tracking with automatic upsert (when document_id is provided) - Temporal and semantic linking - Optional asynchronous processing The system automatically: 1. Extracts semantic facts from the content 2. Generates embeddings 3. Deduplicates similar facts 4. Creates temporal, semantic, and entity links 5. Tracks document metadata When async=true: - Returns immediately after queuing the task - Processing happens in the background - Use the operations endpoint to monitor progress When async=false (default): - Waits for processing to complete - Returns after all memories are stored Note: If document_id is provided and already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior).
|
Retain memory items with automatic fact extraction. This is the main endpoint for storing memories. It supports both synchronous and asynchronous processing via the async parameter. Features: - Efficient batch processing - Automatic fact extraction from natural language - Entity recognition and linking - Document tracking with automatic upsert (when document_id is provided on items) - Temporal and semantic linking - Optional asynchronous processing The system automatically: 1. Extracts semantic facts from the content 2. Generates embeddings 3. Deduplicates similar facts 4. Creates temporal, semantic, and entity links 5. Tracks document metadata When async=true: - Returns immediately after queuing the task - Processing happens in the background - Use the operations endpoint to monitor progress When async=false (default): - Waits for processing to complete - Returns after all memories are stored Note: If a memory item has a document_id that already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior). Items with the same document_id are grouped together for efficient processing.
|
||||||
|
|
||||||
:param bank_id: (required)
|
:param bank_id: (required)
|
||||||
:type bank_id: str
|
:type bank_id: str
|
||||||
|
|
@ -5294,7 +5558,7 @@ class DefaultApi:
|
||||||
) -> RESTResponseType:
|
) -> RESTResponseType:
|
||||||
"""Retain memories
|
"""Retain memories
|
||||||
|
|
||||||
Retain memory items with automatic fact extraction. This is the main endpoint for storing memories. It supports both synchronous and asynchronous processing via the async parameter. Features: - Efficient batch processing - Automatic fact extraction from natural language - Entity recognition and linking - Document tracking with automatic upsert (when document_id is provided) - Temporal and semantic linking - Optional asynchronous processing The system automatically: 1. Extracts semantic facts from the content 2. Generates embeddings 3. Deduplicates similar facts 4. Creates temporal, semantic, and entity links 5. Tracks document metadata When async=true: - Returns immediately after queuing the task - Processing happens in the background - Use the operations endpoint to monitor progress When async=false (default): - Waits for processing to complete - Returns after all memories are stored Note: If document_id is provided and already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior).
|
Retain memory items with automatic fact extraction. This is the main endpoint for storing memories. It supports both synchronous and asynchronous processing via the async parameter. Features: - Efficient batch processing - Automatic fact extraction from natural language - Entity recognition and linking - Document tracking with automatic upsert (when document_id is provided on items) - Temporal and semantic linking - Optional asynchronous processing The system automatically: 1. Extracts semantic facts from the content 2. Generates embeddings 3. Deduplicates similar facts 4. Creates temporal, semantic, and entity links 5. Tracks document metadata When async=true: - Returns immediately after queuing the task - Processing happens in the background - Use the operations endpoint to monitor progress When async=false (default): - Waits for processing to complete - Returns after all memories are stored Note: If a memory item has a document_id that already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior). Items with the same document_id are grouped together for efficient processing.
|
||||||
|
|
||||||
:param bank_id: (required)
|
:param bank_id: (required)
|
||||||
:type bank_id: str
|
:type bank_id: str
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# ChunkResponse
|
||||||
|
|
||||||
|
Response model for get chunk endpoint.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------ | ------------- | ------------- | -------------
|
||||||
|
**chunk_id** | **str** | |
|
||||||
|
**document_id** | **str** | |
|
||||||
|
**bank_id** | **str** | |
|
||||||
|
**chunk_index** | **int** | |
|
||||||
|
**chunk_text** | **str** | |
|
||||||
|
**created_at** | **str** | |
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from hindsight_client_api.models.chunk_response import ChunkResponse
|
||||||
|
|
||||||
|
# TODO update the JSON string below
|
||||||
|
json = "{}"
|
||||||
|
# create an instance of ChunkResponse from a JSON string
|
||||||
|
chunk_response_instance = ChunkResponse.from_json(json)
|
||||||
|
# print the JSON string representation of the object
|
||||||
|
print(ChunkResponse.to_json())
|
||||||
|
|
||||||
|
# convert the object into a dict
|
||||||
|
chunk_response_dict = chunk_response_instance.to_dict()
|
||||||
|
# create an instance of ChunkResponse from a dict
|
||||||
|
chunk_response_from_dict = ChunkResponse.from_dict(chunk_response_dict)
|
||||||
|
```
|
||||||
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@ Method | HTTP request | Description
|
||||||
[**delete_document**](DefaultApi.md#delete_document) | **DELETE** /v1/default/banks/{bank_id}/documents/{document_id} | Delete a document
|
[**delete_document**](DefaultApi.md#delete_document) | **DELETE** /v1/default/banks/{bank_id}/documents/{document_id} | Delete a document
|
||||||
[**get_agent_stats**](DefaultApi.md#get_agent_stats) | **GET** /v1/default/banks/{bank_id}/stats | Get statistics for memory bank
|
[**get_agent_stats**](DefaultApi.md#get_agent_stats) | **GET** /v1/default/banks/{bank_id}/stats | Get statistics for memory bank
|
||||||
[**get_bank_profile**](DefaultApi.md#get_bank_profile) | **GET** /v1/default/banks/{bank_id}/profile | Get memory bank profile
|
[**get_bank_profile**](DefaultApi.md#get_bank_profile) | **GET** /v1/default/banks/{bank_id}/profile | Get memory bank profile
|
||||||
|
[**get_chunk**](DefaultApi.md#get_chunk) | **GET** /v1/default/chunks/{chunk_id} | Get chunk details
|
||||||
[**get_document**](DefaultApi.md#get_document) | **GET** /v1/default/banks/{bank_id}/documents/{document_id} | Get document details
|
[**get_document**](DefaultApi.md#get_document) | **GET** /v1/default/banks/{bank_id}/documents/{document_id} | Get document details
|
||||||
[**get_entity**](DefaultApi.md#get_entity) | **GET** /v1/default/banks/{bank_id}/entities/{entity_id} | Get entity details
|
[**get_entity**](DefaultApi.md#get_entity) | **GET** /v1/default/banks/{bank_id}/entities/{entity_id} | Get entity details
|
||||||
[**get_graph**](DefaultApi.md#get_graph) | **GET** /v1/default/banks/{bank_id}/graph | Get memory graph data
|
[**get_graph**](DefaultApi.md#get_graph) | **GET** /v1/default/banks/{bank_id}/graph | Get memory graph data
|
||||||
|
|
@ -525,6 +526,75 @@ No authorization required
|
||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **get_chunk**
|
||||||
|
> ChunkResponse get_chunk(chunk_id)
|
||||||
|
|
||||||
|
Get chunk details
|
||||||
|
|
||||||
|
Get a specific chunk by its ID
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
import hindsight_client_api
|
||||||
|
from hindsight_client_api.models.chunk_response import ChunkResponse
|
||||||
|
from hindsight_client_api.rest import ApiException
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
# Defining the host is optional and defaults to http://localhost
|
||||||
|
# See configuration.py for a list of all supported configuration parameters.
|
||||||
|
configuration = hindsight_client_api.Configuration(
|
||||||
|
host = "http://localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Enter a context with an instance of the API client
|
||||||
|
async with hindsight_client_api.ApiClient(configuration) as api_client:
|
||||||
|
# Create an instance of the API class
|
||||||
|
api_instance = hindsight_client_api.DefaultApi(api_client)
|
||||||
|
chunk_id = 'chunk_id_example' # str |
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get chunk details
|
||||||
|
api_response = await api_instance.get_chunk(chunk_id)
|
||||||
|
print("The response of DefaultApi->get_chunk:\n")
|
||||||
|
pprint(api_response)
|
||||||
|
except Exception as e:
|
||||||
|
print("Exception when calling DefaultApi->get_chunk: %s\n" % e)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**chunk_id** | **str**| |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**ChunkResponse**](ChunkResponse.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
No authorization required
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
### HTTP response details
|
||||||
|
|
||||||
|
| Status code | Description | Response headers |
|
||||||
|
|-------------|-------------|------------------|
|
||||||
|
**200** | Successful Response | - |
|
||||||
|
**422** | Validation Error | - |
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **get_document**
|
# **get_document**
|
||||||
> DocumentResponse get_document(bank_id, document_id)
|
> DocumentResponse get_document(bank_id, document_id)
|
||||||
|
|
||||||
|
|
@ -1338,7 +1408,7 @@ Retain memory items with automatic fact extraction.
|
||||||
- Efficient batch processing
|
- Efficient batch processing
|
||||||
- Automatic fact extraction from natural language
|
- Automatic fact extraction from natural language
|
||||||
- Entity recognition and linking
|
- Entity recognition and linking
|
||||||
- Document tracking with automatic upsert (when document_id is provided)
|
- Document tracking with automatic upsert (when document_id is provided on items)
|
||||||
- Temporal and semantic linking
|
- Temporal and semantic linking
|
||||||
- Optional asynchronous processing
|
- Optional asynchronous processing
|
||||||
|
|
||||||
|
|
@ -1358,7 +1428,7 @@ Retain memory items with automatic fact extraction.
|
||||||
- Waits for processing to complete
|
- Waits for processing to complete
|
||||||
- Returns after all memories are stored
|
- Returns after all memories are stored
|
||||||
|
|
||||||
Note: If document_id is provided and already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior).
|
Note: If a memory item has a document_id that already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior). Items with the same document_id are grouped together for efficient processing.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ Name | Type | Description | Notes
|
||||||
**timestamp** | **datetime** | | [optional]
|
**timestamp** | **datetime** | | [optional]
|
||||||
**context** | **str** | | [optional]
|
**context** | **str** | | [optional]
|
||||||
**metadata** | **Dict[str, str]** | | [optional]
|
**metadata** | **Dict[str, str]** | | [optional]
|
||||||
|
**document_id** | **str** | | [optional]
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ Options for including additional data in reflect results.
|
||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**facts** | **object** | Options for including facts (based_on) in reflect results. | [optional]
|
**facts** | **object** | Options for including facts (based_on) in reflect results. | [optional]
|
||||||
**entities** | [**EntityIncludeOptions**](EntityIncludeOptions.md) | | [optional]
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ Name | Type | Description | Notes
|
||||||
**budget** | [**Budget**](Budget.md) | | [optional]
|
**budget** | [**Budget**](Budget.md) | | [optional]
|
||||||
**context** | **str** | | [optional]
|
**context** | **str** | | [optional]
|
||||||
**filters** | [**List[MetadataFilter]**](MetadataFilter.md) | | [optional]
|
**filters** | [**List[MetadataFilter]**](MetadataFilter.md) | | [optional]
|
||||||
**include** | [**ReflectIncludeOptions**](ReflectIncludeOptions.md) | Options for including additional data (both disabled by default) | [optional]
|
**include** | [**ReflectIncludeOptions**](ReflectIncludeOptions.md) | Options for including additional data (disabled by default) | [optional]
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ Request model for retain endpoint.
|
||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**items** | [**List[MemoryItem]**](MemoryItem.md) | |
|
**items** | [**List[MemoryItem]**](MemoryItem.md) | |
|
||||||
**document_id** | **str** | | [optional]
|
|
||||||
**var_async** | **bool** | If true, process asynchronously in background. If false, wait for completion (default: false) | [optional] [default to False]
|
**var_async** | **bool** | If true, process asynchronously in background. If false, wait for completion (default: false) | [optional] [default to False]
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**success** | **bool** | |
|
**success** | **bool** | |
|
||||||
**bank_id** | **str** | |
|
**bank_id** | **str** | |
|
||||||
**document_id** | **str** | | [optional]
|
|
||||||
**items_count** | **int** | |
|
**items_count** | **int** | |
|
||||||
**var_async** | **bool** | Whether the operation was processed asynchronously |
|
**var_async** | **bool** | Whether the operation was processed asynchronously |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from hindsight_client_api.models.bank_profile_response import BankProfileRespons
|
||||||
from hindsight_client_api.models.budget import Budget
|
from hindsight_client_api.models.budget import Budget
|
||||||
from hindsight_client_api.models.chunk_data import ChunkData
|
from hindsight_client_api.models.chunk_data import ChunkData
|
||||||
from hindsight_client_api.models.chunk_include_options import ChunkIncludeOptions
|
from hindsight_client_api.models.chunk_include_options import ChunkIncludeOptions
|
||||||
|
from hindsight_client_api.models.chunk_response import ChunkResponse
|
||||||
from hindsight_client_api.models.create_bank_request import CreateBankRequest
|
from hindsight_client_api.models.create_bank_request import CreateBankRequest
|
||||||
from hindsight_client_api.models.delete_response import DeleteResponse
|
from hindsight_client_api.models.delete_response import DeleteResponse
|
||||||
from hindsight_client_api.models.document_response import DocumentResponse
|
from hindsight_client_api.models.document_response import DocumentResponse
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
The version of the OpenAPI document: 1.0.0
|
||||||
|
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||||
|
|
||||||
|
Do not edit the class manually.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import pprint
|
||||||
|
import re # noqa: F401
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr
|
||||||
|
from typing import Any, ClassVar, Dict, List
|
||||||
|
from typing import Optional, Set
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
class ChunkResponse(BaseModel):
|
||||||
|
"""
|
||||||
|
Response model for get chunk endpoint.
|
||||||
|
""" # noqa: E501
|
||||||
|
chunk_id: StrictStr
|
||||||
|
document_id: StrictStr
|
||||||
|
bank_id: StrictStr
|
||||||
|
chunk_index: StrictInt
|
||||||
|
chunk_text: StrictStr
|
||||||
|
created_at: StrictStr
|
||||||
|
__properties: ClassVar[List[str]] = ["chunk_id", "document_id", "bank_id", "chunk_index", "chunk_text", "created_at"]
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
populate_by_name=True,
|
||||||
|
validate_assignment=True,
|
||||||
|
protected_namespaces=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
"""Returns the string representation of the model using alias"""
|
||||||
|
return pprint.pformat(self.model_dump(by_alias=True))
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
"""Returns the JSON representation of the model using alias"""
|
||||||
|
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||||
|
"""Create an instance of ChunkResponse from a JSON string"""
|
||||||
|
return cls.from_dict(json.loads(json_str))
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Return the dictionary representation of the model using alias.
|
||||||
|
|
||||||
|
This has the following differences from calling pydantic's
|
||||||
|
`self.model_dump(by_alias=True)`:
|
||||||
|
|
||||||
|
* `None` is only added to the output dict for nullable fields that
|
||||||
|
were set at model initialization. Other fields with value `None`
|
||||||
|
are ignored.
|
||||||
|
"""
|
||||||
|
excluded_fields: Set[str] = set([
|
||||||
|
])
|
||||||
|
|
||||||
|
_dict = self.model_dump(
|
||||||
|
by_alias=True,
|
||||||
|
exclude=excluded_fields,
|
||||||
|
exclude_none=True,
|
||||||
|
)
|
||||||
|
return _dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||||
|
"""Create an instance of ChunkResponse from a dict"""
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return cls.model_validate(obj)
|
||||||
|
|
||||||
|
_obj = cls.model_validate({
|
||||||
|
"chunk_id": obj.get("chunk_id"),
|
||||||
|
"document_id": obj.get("document_id"),
|
||||||
|
"bank_id": obj.get("bank_id"),
|
||||||
|
"chunk_index": obj.get("chunk_index"),
|
||||||
|
"chunk_text": obj.get("chunk_text"),
|
||||||
|
"created_at": obj.get("created_at")
|
||||||
|
})
|
||||||
|
return _obj
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,7 +31,8 @@ class MemoryItem(BaseModel):
|
||||||
timestamp: Optional[datetime] = None
|
timestamp: Optional[datetime] = None
|
||||||
context: Optional[StrictStr] = None
|
context: Optional[StrictStr] = None
|
||||||
metadata: Optional[Dict[str, StrictStr]] = None
|
metadata: Optional[Dict[str, StrictStr]] = None
|
||||||
__properties: ClassVar[List[str]] = ["content", "timestamp", "context", "metadata"]
|
document_id: Optional[StrictStr] = None
|
||||||
|
__properties: ClassVar[List[str]] = ["content", "timestamp", "context", "metadata", "document_id"]
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
populate_by_name=True,
|
populate_by_name=True,
|
||||||
|
|
@ -87,6 +88,11 @@ class MemoryItem(BaseModel):
|
||||||
if self.metadata is None and "metadata" in self.model_fields_set:
|
if self.metadata is None and "metadata" in self.model_fields_set:
|
||||||
_dict['metadata'] = None
|
_dict['metadata'] = None
|
||||||
|
|
||||||
|
# set to None if document_id (nullable) is None
|
||||||
|
# and model_fields_set contains the field
|
||||||
|
if self.document_id is None and "document_id" in self.model_fields_set:
|
||||||
|
_dict['document_id'] = None
|
||||||
|
|
||||||
return _dict
|
return _dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -102,7 +108,8 @@ class MemoryItem(BaseModel):
|
||||||
"content": obj.get("content"),
|
"content": obj.get("content"),
|
||||||
"timestamp": obj.get("timestamp"),
|
"timestamp": obj.get("timestamp"),
|
||||||
"context": obj.get("context"),
|
"context": obj.get("context"),
|
||||||
"metadata": obj.get("metadata")
|
"metadata": obj.get("metadata"),
|
||||||
|
"document_id": obj.get("document_id")
|
||||||
})
|
})
|
||||||
return _obj
|
return _obj
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import json
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
from typing import Any, ClassVar, Dict, List, Optional
|
from typing import Any, ClassVar, Dict, List, Optional
|
||||||
from hindsight_client_api.models.entity_include_options import EntityIncludeOptions
|
|
||||||
from typing import Optional, Set
|
from typing import Optional, Set
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
|
@ -28,8 +27,7 @@ class ReflectIncludeOptions(BaseModel):
|
||||||
Options for including additional data in reflect results.
|
Options for including additional data in reflect results.
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
facts: Optional[Dict[str, Any]] = Field(default=None, description="Options for including facts (based_on) in reflect results.")
|
facts: Optional[Dict[str, Any]] = Field(default=None, description="Options for including facts (based_on) in reflect results.")
|
||||||
entities: Optional[EntityIncludeOptions] = None
|
__properties: ClassVar[List[str]] = ["facts"]
|
||||||
__properties: ClassVar[List[str]] = ["facts", "entities"]
|
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
populate_by_name=True,
|
populate_by_name=True,
|
||||||
|
|
@ -70,14 +68,6 @@ class ReflectIncludeOptions(BaseModel):
|
||||||
exclude=excluded_fields,
|
exclude=excluded_fields,
|
||||||
exclude_none=True,
|
exclude_none=True,
|
||||||
)
|
)
|
||||||
# override the default output from pydantic by calling `to_dict()` of entities
|
|
||||||
if self.entities:
|
|
||||||
_dict['entities'] = self.entities.to_dict()
|
|
||||||
# set to None if entities (nullable) is None
|
|
||||||
# and model_fields_set contains the field
|
|
||||||
if self.entities is None and "entities" in self.model_fields_set:
|
|
||||||
_dict['entities'] = None
|
|
||||||
|
|
||||||
return _dict
|
return _dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -90,8 +80,7 @@ class ReflectIncludeOptions(BaseModel):
|
||||||
return cls.model_validate(obj)
|
return cls.model_validate(obj)
|
||||||
|
|
||||||
_obj = cls.model_validate({
|
_obj = cls.model_validate({
|
||||||
"facts": obj.get("facts"),
|
"facts": obj.get("facts")
|
||||||
"entities": EntityIncludeOptions.from_dict(obj["entities"]) if obj.get("entities") is not None else None
|
|
||||||
})
|
})
|
||||||
return _obj
|
return _obj
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class ReflectRequest(BaseModel):
|
||||||
budget: Optional[Budget] = None
|
budget: Optional[Budget] = None
|
||||||
context: Optional[StrictStr] = None
|
context: Optional[StrictStr] = None
|
||||||
filters: Optional[List[MetadataFilter]] = None
|
filters: Optional[List[MetadataFilter]] = None
|
||||||
include: Optional[ReflectIncludeOptions] = Field(default=None, description="Options for including additional data (both disabled by default)")
|
include: Optional[ReflectIncludeOptions] = Field(default=None, description="Options for including additional data (disabled by default)")
|
||||||
__properties: ClassVar[List[str]] = ["query", "budget", "context", "filters", "include"]
|
__properties: ClassVar[List[str]] = ["query", "budget", "context", "filters", "include"]
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import pprint
|
||||||
import re # noqa: F401
|
import re # noqa: F401
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr
|
from pydantic import BaseModel, ConfigDict, Field, StrictBool
|
||||||
from typing import Any, ClassVar, Dict, List, Optional
|
from typing import Any, ClassVar, Dict, List, Optional
|
||||||
from hindsight_client_api.models.memory_item import MemoryItem
|
from hindsight_client_api.models.memory_item import MemoryItem
|
||||||
from typing import Optional, Set
|
from typing import Optional, Set
|
||||||
|
|
@ -28,9 +28,8 @@ class RetainRequest(BaseModel):
|
||||||
Request model for retain endpoint.
|
Request model for retain endpoint.
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
items: List[MemoryItem]
|
items: List[MemoryItem]
|
||||||
document_id: Optional[StrictStr] = None
|
|
||||||
var_async: Optional[StrictBool] = Field(default=False, description="If true, process asynchronously in background. If false, wait for completion (default: false)", alias="async")
|
var_async: Optional[StrictBool] = Field(default=False, description="If true, process asynchronously in background. If false, wait for completion (default: false)", alias="async")
|
||||||
__properties: ClassVar[List[str]] = ["items", "document_id", "async"]
|
__properties: ClassVar[List[str]] = ["items", "async"]
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
populate_by_name=True,
|
populate_by_name=True,
|
||||||
|
|
@ -78,11 +77,6 @@ class RetainRequest(BaseModel):
|
||||||
if _item_items:
|
if _item_items:
|
||||||
_items.append(_item_items.to_dict())
|
_items.append(_item_items.to_dict())
|
||||||
_dict['items'] = _items
|
_dict['items'] = _items
|
||||||
# set to None if document_id (nullable) is None
|
|
||||||
# and model_fields_set contains the field
|
|
||||||
if self.document_id is None and "document_id" in self.model_fields_set:
|
|
||||||
_dict['document_id'] = None
|
|
||||||
|
|
||||||
return _dict
|
return _dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -96,7 +90,6 @@ class RetainRequest(BaseModel):
|
||||||
|
|
||||||
_obj = cls.model_validate({
|
_obj = cls.model_validate({
|
||||||
"items": [MemoryItem.from_dict(_item) for _item in obj["items"]] if obj.get("items") is not None else None,
|
"items": [MemoryItem.from_dict(_item) for _item in obj["items"]] if obj.get("items") is not None else None,
|
||||||
"document_id": obj.get("document_id"),
|
|
||||||
"async": obj.get("async") if obj.get("async") is not None else False
|
"async": obj.get("async") if obj.get("async") is not None else False
|
||||||
})
|
})
|
||||||
return _obj
|
return _obj
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import re # noqa: F401
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr
|
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr
|
||||||
from typing import Any, ClassVar, Dict, List, Optional
|
from typing import Any, ClassVar, Dict, List
|
||||||
from typing import Optional, Set
|
from typing import Optional, Set
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
|
@ -28,10 +28,9 @@ class RetainResponse(BaseModel):
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
success: StrictBool
|
success: StrictBool
|
||||||
bank_id: StrictStr
|
bank_id: StrictStr
|
||||||
document_id: Optional[StrictStr] = None
|
|
||||||
items_count: StrictInt
|
items_count: StrictInt
|
||||||
var_async: StrictBool = Field(description="Whether the operation was processed asynchronously", alias="async")
|
var_async: StrictBool = Field(description="Whether the operation was processed asynchronously", alias="async")
|
||||||
__properties: ClassVar[List[str]] = ["success", "bank_id", "document_id", "items_count", "async"]
|
__properties: ClassVar[List[str]] = ["success", "bank_id", "items_count", "async"]
|
||||||
|
|
||||||
model_config = ConfigDict(
|
model_config = ConfigDict(
|
||||||
populate_by_name=True,
|
populate_by_name=True,
|
||||||
|
|
@ -72,11 +71,6 @@ class RetainResponse(BaseModel):
|
||||||
exclude=excluded_fields,
|
exclude=excluded_fields,
|
||||||
exclude_none=True,
|
exclude_none=True,
|
||||||
)
|
)
|
||||||
# set to None if document_id (nullable) is None
|
|
||||||
# and model_fields_set contains the field
|
|
||||||
if self.document_id is None and "document_id" in self.model_fields_set:
|
|
||||||
_dict['document_id'] = None
|
|
||||||
|
|
||||||
return _dict
|
return _dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -91,7 +85,6 @@ class RetainResponse(BaseModel):
|
||||||
_obj = cls.model_validate({
|
_obj = cls.model_validate({
|
||||||
"success": obj.get("success"),
|
"success": obj.get("success"),
|
||||||
"bank_id": obj.get("bank_id"),
|
"bank_id": obj.get("bank_id"),
|
||||||
"document_id": obj.get("document_id"),
|
|
||||||
"items_count": obj.get("items_count"),
|
"items_count": obj.get("items_count"),
|
||||||
"async": obj.get("async")
|
"async": obj.get("async")
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hindsight HTTP API
|
||||||
|
|
||||||
|
HTTP API for Hindsight
|
||||||
|
|
||||||
|
The version of the OpenAPI document: 1.0.0
|
||||||
|
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||||
|
|
||||||
|
Do not edit the class manually.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from hindsight_client_api.models.chunk_response import ChunkResponse
|
||||||
|
|
||||||
|
class TestChunkResponse(unittest.TestCase):
|
||||||
|
"""ChunkResponse unit test stubs"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def make_instance(self, include_optional) -> ChunkResponse:
|
||||||
|
"""Test ChunkResponse
|
||||||
|
include_optional is a boolean, when False only required
|
||||||
|
params are included, when True both required and
|
||||||
|
optional params are included """
|
||||||
|
# uncomment below to create an instance of `ChunkResponse`
|
||||||
|
"""
|
||||||
|
model = ChunkResponse()
|
||||||
|
if include_optional:
|
||||||
|
return ChunkResponse(
|
||||||
|
chunk_id = '',
|
||||||
|
document_id = '',
|
||||||
|
bank_id = '',
|
||||||
|
chunk_index = 56,
|
||||||
|
chunk_text = '',
|
||||||
|
created_at = ''
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ChunkResponse(
|
||||||
|
chunk_id = '',
|
||||||
|
document_id = '',
|
||||||
|
bank_id = '',
|
||||||
|
chunk_index = 56,
|
||||||
|
chunk_text = '',
|
||||||
|
created_at = '',
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def testChunkResponse(self):
|
||||||
|
"""Test ChunkResponse"""
|
||||||
|
# inst_req_only = self.make_instance(include_optional=False)
|
||||||
|
# inst_req_and_optional = self.make_instance(include_optional=True)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
@ -75,6 +75,13 @@ class TestDefaultApi(unittest.IsolatedAsyncioTestCase):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def test_get_chunk(self) -> None:
|
||||||
|
"""Test case for get_chunk
|
||||||
|
|
||||||
|
Get chunk details
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
async def test_get_document(self) -> None:
|
async def test_get_document(self) -> None:
|
||||||
"""Test case for get_document
|
"""Test case for get_document
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ class TestMemoryItem(unittest.TestCase):
|
||||||
context = '',
|
context = '',
|
||||||
metadata = {
|
metadata = {
|
||||||
'key' : ''
|
'key' : ''
|
||||||
}
|
},
|
||||||
|
document_id = ''
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return MemoryItem(
|
return MemoryItem(
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,7 @@ class TestReflectIncludeOptions(unittest.TestCase):
|
||||||
model = ReflectIncludeOptions()
|
model = ReflectIncludeOptions()
|
||||||
if include_optional:
|
if include_optional:
|
||||||
return ReflectIncludeOptions(
|
return ReflectIncludeOptions(
|
||||||
facts = hindsight_client_api.models.facts_include_options.FactsIncludeOptions(),
|
facts = hindsight_client_api.models.facts_include_options.FactsIncludeOptions()
|
||||||
entities = hindsight_client_api.models.entity_include_options.EntityIncludeOptions(
|
|
||||||
max_tokens = 56, )
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return ReflectIncludeOptions(
|
return ReflectIncludeOptions(
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,7 @@ class TestReflectRequest(unittest.TestCase):
|
||||||
{key=source, match_unset=true, value=slack}
|
{key=source, match_unset=true, value=slack}
|
||||||
],
|
],
|
||||||
include = hindsight_client_api.models.reflect_include_options.ReflectIncludeOptions(
|
include = hindsight_client_api.models.reflect_include_options.ReflectIncludeOptions(
|
||||||
facts = hindsight_client_api.models.facts_include_options.FactsIncludeOptions(),
|
facts = hindsight_client_api.models.facts_include_options.FactsIncludeOptions(), )
|
||||||
entities = hindsight_client_api.models.entity_include_options.EntityIncludeOptions(
|
|
||||||
max_tokens = 56, ), )
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return ReflectRequest(
|
return ReflectRequest(
|
||||||
|
|
|
||||||
|
|
@ -36,15 +36,14 @@ class TestRetainRequest(unittest.TestCase):
|
||||||
if include_optional:
|
if include_optional:
|
||||||
return RetainRequest(
|
return RetainRequest(
|
||||||
items = [
|
items = [
|
||||||
{content=Alice mentioned she's working on a new ML model, context=team meeting, metadata={channel=engineering, source=slack}, timestamp=2024-01-15T10:30:00Z}
|
{content=Alice mentioned she's working on a new ML model, context=team meeting, document_id=meeting_notes_2024_01_15, metadata={channel=engineering, source=slack}, timestamp=2024-01-15T10:30:00Z}
|
||||||
],
|
],
|
||||||
document_id = '',
|
|
||||||
var_async = True
|
var_async = True
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return RetainRequest(
|
return RetainRequest(
|
||||||
items = [
|
items = [
|
||||||
{content=Alice mentioned she's working on a new ML model, context=team meeting, metadata={channel=engineering, source=slack}, timestamp=2024-01-15T10:30:00Z}
|
{content=Alice mentioned she's working on a new ML model, context=team meeting, document_id=meeting_notes_2024_01_15, metadata={channel=engineering, source=slack}, timestamp=2024-01-15T10:30:00Z}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ class TestRetainResponse(unittest.TestCase):
|
||||||
return RetainResponse(
|
return RetainResponse(
|
||||||
success = True,
|
success = True,
|
||||||
bank_id = '',
|
bank_id = '',
|
||||||
document_id = '',
|
|
||||||
items_count = 56,
|
items_count = 56,
|
||||||
var_async = True
|
var_async = True
|
||||||
)
|
)
|
||||||
|
|
|
||||||
4
hindsight-clients/rust/Cargo.lock
generated
4
hindsight-clients/rust/Cargo.lock
generated
|
|
@ -1680,9 +1680,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.18.1"
|
version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
49b8126e9760ef6c
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
15024a40d442b93a
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"rustc":16243257175721966122,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[350039288653093011,"build_script_build",false,7490509127350218047]],"local":[{"RerunIfChanged":{"output":"release/build/hindsight-client-45e816fa8febabac/output","paths":["/Users/nicoloboschi/dev/memory-poc/openapi.json"]}}],"rustflags":[],"config":0,"compile_kind":0}
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
92e66ba8891fb9ec
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":5408242616063297496,"profile":1369601567987815722,"path":13767053534773805487,"deps":[[7988640081342112296,"syn",false,1809862803220272063],[9423015880379144908,"prettyplease",false,5158570001680563136],[9738901266855342370,"progenitor",false,5156049257603408733],[12832915883349295919,"serde_json",false,7203318985267246464],[16847286912798951732,"openapiv3",false,15609397813544834071]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/hindsight-client-b1a7aa8cd48fa221/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":5408242616063297496,"profile":1369601567987815722,"path":13767053534773805487,"deps":[[7988640081342112296,"syn",false,1809862803220272063],[9423015880379144908,"prettyplease",false,5158570001680563136],[9738901266855342370,"progenitor",false,6987356646142300397],[12832915883349295919,"serde_json",false,7203318985267246464],[16847286912798951732,"openapiv3",false,15609397813544834071]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/hindsight-client-58618863206160a4/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
17b2b80455e1755a
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":16243257175721966122,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[350039288653093011,"build_script_build",false,17057699739739088530]],"local":[{"RerunIfChanged":{"output":"release/build/hindsight-client-793f05aca97996e5/output","paths":["/Users/nicoloboschi/dev/memory-poc/openapi.json"]}}],"rustflags":[],"config":0,"compile_kind":0}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
3fed876a5da2f367
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
b85c02be490b8ea0
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":6828276606420267087,"profile":2040997289075261528,"path":10763286916239946207,"deps":[[350039288653093011,"build_script_build",false,4231486804270383637],[503842845364652431,"chrono",false,11144948474405894614],[1046219396048762255,"progenitor_client",false,13036815077551201483],[2620434475832828286,"http",false,9979032511492736550],[5404511084185685755,"url",false,17079058419592311478],[5802782114936492624,"reqwest",false,16326245460864990945],[7720834239451334583,"tokio",false,814226396053303386],[8008191657135824715,"thiserror",false,1675330904495433212],[12832915883349295919,"serde_json",false,11663418101978700483],[13548984313718623784,"serde",false,17261882564294632758]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/hindsight-client-26b0bc308cced3ae/dep-lib-hindsight_client","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":6828276606420267087,"profile":2040997289075261528,"path":10763286916239946207,"deps":[[350039288653093011,"build_script_build",false,6518363790956343831],[503842845364652431,"chrono",false,11144948474405894614],[1046219396048762255,"progenitor_client",false,13036815077551201483],[2620434475832828286,"http",false,9979032511492736550],[5404511084185685755,"url",false,17079058419592311478],[5802782114936492624,"reqwest",false,16326245460864990945],[7720834239451334583,"tokio",false,814226396053303386],[8008191657135824715,"thiserror",false,1675330904495433212],[12832915883349295919,"serde_json",false,11663418101978700483],[13548984313718623784,"serde",false,17261882564294632758]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/hindsight-client-b2e1c0bce7e404b8/dep-lib-hindsight_client","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
edd0465cd913f860
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"rustc":16243257175721966122,"features":"[\"default\", \"macro\"]","declared_features":"[\"default\", \"macro\"]","target":15608857702111660434,"profile":1369601567987815722,"path":14901966660390340651,"deps":[[1046219396048762255,"progenitor_client",false,13306704853339479171],[3039535961030183584,"progenitor_impl",false,636270600742616262],[17067139923740644357,"progenitor_macro",false,179726542608778300]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/progenitor-4bdd0c7fc86c6021/dep-lib-progenitor","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
{"rustc":16243257175721966122,"features":"[\"default\", \"macro\"]","declared_features":"[\"default\", \"macro\"]","target":15608857702111660434,"profile":1369601567987815722,"path":14901966660390340651,"deps":[[1046219396048762255,"progenitor_client",false,13306704853339479171],[3039535961030183584,"progenitor_impl",false,3223650479284347882],[17067139923740644357,"progenitor_macro",false,7805686184841859657]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/progenitor-09c624ef88805fe3/dep-lib-progenitor","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
5d23de5b7bf78d47
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
eae3c20108b5bc2c
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":14575771264175982451,"profile":1369601567987815722,"path":4143174102873047870,"deps":[[1548027836057496652,"unicode_ident",false,12497779118727399146],[2620434475832828286,"http",false,1650492083869495292],[3056178850035811329,"regex",false,16159500298532302859],[4336745513838352383,"thiserror",false,16080867181668872609],[6240934600354534560,"indexmap",false,12677675679014318805],[6913375703034175521,"schemars",false,374665543383128733],[7988640081342112296,"syn",false,1809862803220272063],[9869581871423326951,"quote",false,16408282429201193700],[11401754758611382041,"typify",false,17824555848214860990],[12832915883349295919,"serde_json",false,7203318985267246464],[13077543566650298139,"heck",false,13265169220388563925],[13548984313718623784,"serde",false,18392579626400240657],[14285738760999836560,"proc_macro2",false,14273862529107951632],[16847286912798951732,"openapiv3",false,15609397813544834071]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/progenitor-impl-06826d04079242ad/dep-lib-progenitor_impl","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
c6bc4982bd7cd408
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":14575771264175982451,"profile":1369601567987815722,"path":4143174102873047870,"deps":[[1548027836057496652,"unicode_ident",false,12497779118727399146],[2620434475832828286,"http",false,1650492083869495292],[3056178850035811329,"regex",false,16159500298532302859],[4336745513838352383,"thiserror",false,16080867181668872609],[6240934600354534560,"indexmap",false,12677675679014318805],[6913375703034175521,"schemars",false,15965657796090931825],[7988640081342112296,"syn",false,1809862803220272063],[9869581871423326951,"quote",false,16408282429201193700],[11401754758611382041,"typify",false,1878054387901356869],[12832915883349295919,"serde_json",false,7203318985267246464],[13077543566650298139,"heck",false,13265169220388563925],[13548984313718623784,"serde",false,18392579626400240657],[14285738760999836560,"proc_macro2",false,14273862529107951632],[16847286912798951732,"openapiv3",false,15609397813544834071]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/progenitor-impl-4be51208161b9376/dep-lib-progenitor_impl","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
4952ac933a5e536c
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":12433518205030116463,"profile":1369601567987815722,"path":3059927606038109866,"deps":[[3039535961030183584,"progenitor_impl",false,3223650479284347882],[6913375703034175521,"schemars",false,374665543383128733],[7988640081342112296,"syn",false,1809862803220272063],[9614479274285663593,"serde_yaml",false,166363834370521707],[9869581871423326951,"quote",false,16408282429201193700],[12832915883349295919,"serde_json",false,7203318985267246464],[13548984313718623784,"serde",false,18392579626400240657],[14285738760999836560,"proc_macro2",false,14273862529107951632],[16847286912798951732,"openapiv3",false,15609397813544834071],[18142522549889578203,"serde_tokenstream",false,16295073279524653793]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/progenitor-macro-2aa1b4026eb3ec1c/dep-lib-progenitor_macro","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
3cc4e79856847e02
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":12433518205030116463,"profile":1369601567987815722,"path":3059927606038109866,"deps":[[3039535961030183584,"progenitor_impl",false,636270600742616262],[6913375703034175521,"schemars",false,15965657796090931825],[7988640081342112296,"syn",false,1809862803220272063],[9614479274285663593,"serde_yaml",false,166363834370521707],[9869581871423326951,"quote",false,16408282429201193700],[12832915883349295919,"serde_json",false,7203318985267246464],[13548984313718623784,"serde",false,18392579626400240657],[14285738760999836560,"proc_macro2",false,14273862529107951632],[16847286912798951732,"openapiv3",false,15609397813544834071],[18142522549889578203,"serde_tokenstream",false,16295073279524653793]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/progenitor-macro-be18053f5df03ea5/dep-lib-progenitor_macro","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
712a72052d6991dd
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
9d1aee9e53143305
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"rustc":16243257175721966122,"features":"[\"chrono\", \"default\", \"derive\", \"schemars_derive\", \"uuid1\"]","declared_features":"[\"arrayvec\", \"arrayvec05\", \"arrayvec07\", \"bigdecimal\", \"bigdecimal03\", \"bigdecimal04\", \"bytes\", \"chrono\", \"default\", \"derive\", \"derive_json_schema\", \"either\", \"enumset\", \"impl_json_schema\", \"indexmap\", \"indexmap1\", \"indexmap2\", \"preserve_order\", \"raw_value\", \"rust_decimal\", \"schemars_derive\", \"semver\", \"smallvec\", \"smol_str\", \"ui_test\", \"url\", \"uuid\", \"uuid08\", \"uuid1\"]","target":11155677158530064643,"profile":1369601567987815722,"path":5926874010205357219,"deps":[[503842845364652431,"chrono",false,2900766844280536775],[6913375703034175521,"build_script_build",false,7206007404470304092],[6982418085031928086,"dyn_clone",false,6834683740842263458],[12832915883349295919,"serde_json",false,7203318985267246464],[13548984313718623784,"serde",false,18392579626400240657],[15267671913832104935,"uuid1",false,12241668191619465921],[16071897500792579091,"schemars_derive",false,1760453691084183604]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/schemars-1d824015212552b8/dep-lib-schemars","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
{"rustc":16243257175721966122,"features":"[\"chrono\", \"default\", \"derive\", \"schemars_derive\", \"uuid1\"]","declared_features":"[\"arrayvec\", \"arrayvec05\", \"arrayvec07\", \"bigdecimal\", \"bigdecimal03\", \"bigdecimal04\", \"bytes\", \"chrono\", \"default\", \"derive\", \"derive_json_schema\", \"either\", \"enumset\", \"impl_json_schema\", \"indexmap\", \"indexmap1\", \"indexmap2\", \"preserve_order\", \"raw_value\", \"rust_decimal\", \"schemars_derive\", \"semver\", \"smallvec\", \"smol_str\", \"ui_test\", \"url\", \"uuid\", \"uuid08\", \"uuid1\"]","target":11155677158530064643,"profile":1369601567987815722,"path":5926874010205357219,"deps":[[503842845364652431,"chrono",false,2900766844280536775],[1420800981318104879,"uuid1",false,18252328355564986311],[6913375703034175521,"build_script_build",false,7206007404470304092],[6982418085031928086,"dyn_clone",false,6834683740842263458],[12832915883349295919,"serde_json",false,7203318985267246464],[13548984313718623784,"serde",false,18392579626400240657],[16071897500792579091,"schemars_derive",false,1760453691084183604]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/schemars-2baa628a98a02c17/dep-lib-schemars","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
be54f5fb1a8b5df7
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"rustc":16243257175721966122,"features":"[\"default\", \"macro\", \"typify-macro\"]","declared_features":"[\"default\", \"macro\", \"typify-macro\"]","target":14975903297306792855,"profile":1369601567987815722,"path":17345980241392545380,"deps":[[12189557469245296852,"typify_impl",false,11312802444564477127],[12514255388840618205,"typify_macro",false,8783384910811377035]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/typify-65e7243859581cdf/dep-lib-typify","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
{"rustc":16243257175721966122,"features":"[\"default\", \"macro\", \"typify-macro\"]","declared_features":"[\"default\", \"macro\", \"typify-macro\"]","target":14975903297306792855,"profile":1369601567987815722,"path":17345980241392545380,"deps":[[12189557469245296852,"typify_impl",false,8737027169081722424],[12514255388840618205,"typify_macro",false,17335598172921498906]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/typify-3c13a635718c0fbd/dep-lib-typify","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
455bb9f38330101a
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
38860d66eb274079
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":2642133076118073701,"profile":1369601567987815722,"path":17859204502057325984,"deps":[[57391913602052214,"regress",false,5853959305665143200],[1548027836057496652,"unicode_ident",false,12497779118727399146],[4336745513838352383,"thiserror",false,16080867181668872609],[6913375703034175521,"schemars",false,374665543383128733],[7988640081342112296,"syn",false,1809862803220272063],[9869581871423326951,"quote",false,16408282429201193700],[12832915883349295919,"serde_json",false,7203318985267246464],[13066042571740262168,"log",false,5499292635580693977],[13077543566650298139,"heck",false,13265169220388563925],[13548984313718623784,"serde",false,18392579626400240657],[14285738760999836560,"proc_macro2",false,14273862529107951632],[18361894353739432590,"semver",false,6556068567130520783]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/typify-impl-c6bad80e900da2bc/dep-lib-typify_impl","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
c7b46fb2e225ff9c
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":2642133076118073701,"profile":1369601567987815722,"path":17859204502057325984,"deps":[[57391913602052214,"regress",false,5853959305665143200],[1548027836057496652,"unicode_ident",false,12497779118727399146],[4336745513838352383,"thiserror",false,16080867181668872609],[6913375703034175521,"schemars",false,15965657796090931825],[7988640081342112296,"syn",false,1809862803220272063],[9869581871423326951,"quote",false,16408282429201193700],[12832915883349295919,"serde_json",false,7203318985267246464],[13066042571740262168,"log",false,5499292635580693977],[13077543566650298139,"heck",false,13265169220388563925],[13548984313718623784,"serde",false,18392579626400240657],[14285738760999836560,"proc_macro2",false,14273862529107951632],[18361894353739432590,"semver",false,6556068567130520783]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/typify-impl-eae5a0de0558fb19/dep-lib-typify_impl","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
1a8de5b9b06a94f0
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":4711089848534984104,"profile":1369601567987815722,"path":16211961820950626129,"deps":[[6913375703034175521,"schemars",false,374665543383128733],[7988640081342112296,"syn",false,1809862803220272063],[9869581871423326951,"quote",false,16408282429201193700],[12189557469245296852,"typify_impl",false,8737027169081722424],[12832915883349295919,"serde_json",false,7203318985267246464],[13548984313718623784,"serde",false,18392579626400240657],[14285738760999836560,"proc_macro2",false,14273862529107951632],[18142522549889578203,"serde_tokenstream",false,16295073279524653793],[18361894353739432590,"semver",false,6556068567130520783]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/typify-macro-13de632972e7e35b/dep-lib-typify_macro","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
8b2d703e0adae479
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":4711089848534984104,"profile":1369601567987815722,"path":16211961820950626129,"deps":[[6913375703034175521,"schemars",false,15965657796090931825],[7988640081342112296,"syn",false,1809862803220272063],[9869581871423326951,"quote",false,16408282429201193700],[12189557469245296852,"typify_impl",false,11312802444564477127],[12832915883349295919,"serde_json",false,7203318985267246464],[13548984313718623784,"serde",false,18392579626400240657],[14285738760999836560,"proc_macro2",false,14273862529107951632],[18142522549889578203,"serde_tokenstream",false,16295073279524653793],[18361894353739432590,"semver",false,6556068567130520783]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/typify-macro-fd19a21f23250962/dep-lib-typify_macro","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
c79f3d35ef4b4dfd
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rustc":16243257175721966122,"features":"[]","declared_features":"[\"arbitrary\", \"atomic\", \"borsh\", \"bytemuck\", \"default\", \"fast-rng\", \"js\", \"macro-diagnostics\", \"md5\", \"rng\", \"rng-getrandom\", \"rng-rand\", \"serde\", \"sha1\", \"slog\", \"std\", \"uuid-rng-internal-lib\", \"v1\", \"v3\", \"v4\", \"v5\", \"v6\", \"v7\", \"v8\", \"zerocopy\"]","target":2422778461497348360,"profile":10765049016586272810,"path":2526824666322011227,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/uuid-892ce5c89892b489/dep-lib-uuid","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
c142ad735c24e3a9
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue