Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45ed02cd6a | ||
|
|
2921fa091b | ||
|
|
aafa7ae17c | ||
|
|
caeab8ac44 | ||
|
|
42cc098a7c | ||
|
|
2871ddc34c | ||
|
|
987b47e6a1 | ||
|
|
25fb4b9273 | ||
|
|
f72c9f03fb | ||
|
|
517eee4eda | ||
|
|
e19c6b9252 |
+26
-14
@@ -153,15 +153,8 @@ jobs:
|
||||
- name: Build docs
|
||||
run: npm run build --workspace=hindsight-docs
|
||||
|
||||
test-rust-cli:
|
||||
build-rust-cli:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
HINDSIGHT_API_LLM_PROVIDER: groq
|
||||
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
HINDSIGHT_API_URL: http://localhost:8888
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -178,10 +171,6 @@ jobs:
|
||||
hindsight-cli/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Run unit tests
|
||||
working-directory: hindsight-cli
|
||||
run: cargo test
|
||||
|
||||
- name: Build CLI
|
||||
working-directory: hindsight-cli
|
||||
run: cargo build --release
|
||||
@@ -193,6 +182,29 @@ jobs:
|
||||
path: hindsight-cli/target/release/hindsight
|
||||
retention-days: 1
|
||||
|
||||
test-rust-cli:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-rust-cli
|
||||
env:
|
||||
HINDSIGHT_API_LLM_PROVIDER: groq
|
||||
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
HINDSIGHT_API_URL: http://localhost:8888
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download CLI artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: hindsight-cli
|
||||
path: /tmp/cli
|
||||
|
||||
- name: Make CLI executable
|
||||
run: chmod +x /tmp/cli/hindsight
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
@@ -239,7 +251,7 @@ jobs:
|
||||
|
||||
- name: Run CLI smoke test
|
||||
run: |
|
||||
HINDSIGHT_CLI=hindsight-cli/target/release/hindsight ./hindsight-cli/smoke-test.sh
|
||||
HINDSIGHT_CLI=/tmp/cli/hindsight ./hindsight-cli/smoke-test.sh
|
||||
|
||||
- name: Show API server logs
|
||||
if: always()
|
||||
@@ -765,7 +777,7 @@ jobs:
|
||||
|
||||
test-doc-examples:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test-rust-cli
|
||||
needs: build-rust-cli
|
||||
env:
|
||||
HINDSIGHT_API_LLM_PROVIDER: groq
|
||||
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ nltk_data/
|
||||
|
||||
# Monitoring stack (Prometheus/Grafana binaries and data)
|
||||
.monitoring/
|
||||
.pgbouncer/
|
||||
.pgbouncer
|
||||
|
||||
# Large benchmark datasets (will be downloaded automatically)
|
||||
**/longmemeval_s_cleaned.json
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
[databases]
|
||||
; Connect to pg0 on port 5433
|
||||
; The actual pg0 database is called "hindsight"
|
||||
hindsight = host=127.0.0.1 port=5433 dbname=hindsight user=hindsight password=hindsight
|
||||
|
||||
[pgbouncer]
|
||||
listen_addr = 127.0.0.1
|
||||
listen_port = 6432
|
||||
|
||||
; Use md5 authentication (matches pg0's auth)
|
||||
auth_type = md5
|
||||
auth_file = /Users/nicoloboschi/dev/memory-poc/.pgbouncer/userlist.txt
|
||||
|
||||
; Transaction pooling mode (recommended for hindsight)
|
||||
pool_mode = transaction
|
||||
|
||||
; Reset connection state after each transaction
|
||||
server_reset_query = DISCARD ALL
|
||||
|
||||
; Pool sizing
|
||||
default_pool_size = 20
|
||||
max_client_conn = 200
|
||||
min_pool_size = 5
|
||||
|
||||
; Timeouts
|
||||
server_idle_timeout = 600
|
||||
server_lifetime = 3600
|
||||
query_timeout = 120
|
||||
|
||||
; Logging
|
||||
log_connections = 1
|
||||
log_disconnections = 1
|
||||
log_pooler_errors = 1
|
||||
|
||||
; Stats
|
||||
stats_period = 60
|
||||
|
||||
; Admin console
|
||||
admin_users = admin
|
||||
@@ -0,0 +1,2 @@
|
||||
"hindsight" "md5d842ccb6249bcd3c53b2f648378092a6"
|
||||
"admin" ""
|
||||
@@ -1,3 +1,153 @@
|
||||
# AGENTS.md
|
||||
|
||||
See [CLAUDE.md](./CLAUDE.md) for project documentation and coding conventions.
|
||||
This document captures architectural decisions and coding conventions for the Hindsight project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Main documentation**: [hindsight-docs/docs/developer/](./hindsight-docs/docs/developer/)
|
||||
- **Use case patterns**: [hindsight-docs/docs/cookbook/](./hindsight-docs/docs/cookbook/)
|
||||
- **API reference**: Auto-generated from OpenAPI spec
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
hindsight/ # Python package for embedded usage
|
||||
hindsight-api/ # FastAPI server (core memory engine)
|
||||
hindsight-cli/ # Rust CLI client
|
||||
hindsight-embed/ # Embedded CLI (no server needed)
|
||||
hindsight-control-plane/ # Next.js admin UI
|
||||
hindsight-docs/ # Docusaurus documentation site
|
||||
hindsight-dev/ # Development tools and benchmarks
|
||||
hindsight-integrations/ # Framework integrations (LangChain, etc.)
|
||||
hindsight-clients/ # Generated API clients (Python, TypeScript, Rust)
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Memory Banks
|
||||
- Each bank is an isolated memory store (like a "brain" for one user/agent)
|
||||
- Banks contain: memory units (facts), entities, documents, entity links
|
||||
- Banks have a **disposition** (personality traits) and **background** (context)
|
||||
- Bank isolation is strict - no cross-bank data leakage
|
||||
|
||||
### Memory Types
|
||||
- **World facts**: General knowledge ("The sky is blue")
|
||||
- **Experience facts**: Personal experiences ("I visited Paris in 2023")
|
||||
- **Opinion facts**: Beliefs with confidence scores ("Paris is beautiful" - 0.9 confidence)
|
||||
|
||||
### Operations
|
||||
- **Retain**: Store new memories (extracts facts, entities, relationships)
|
||||
- **Recall**: Retrieve memories (semantic, BM25, graph, temporal search)
|
||||
- **Reflect**: Deep analysis to form new insights/opinions
|
||||
|
||||
## API Design Decisions
|
||||
|
||||
### Single Bank Per Request
|
||||
- All API endpoints (`recall`, `reflect`, `retain`) operate on a single bank
|
||||
- Multi-bank queries are the **client/agent's responsibility** to orchestrate
|
||||
- This keeps the API simple and the isolation model clear
|
||||
|
||||
### Disposition Traits (3-trait system)
|
||||
- **Skepticism** (1-5): How skeptical vs trusting when forming opinions
|
||||
- **Literalism** (1-5): How literally to interpret information
|
||||
- **Empathy** (1-5): How much to consider emotional context
|
||||
- These influence the `reflect` operation, not `recall`
|
||||
- Background info also only affects `reflect` (opinion formation)
|
||||
|
||||
## Multi-Bank Architecture Patterns
|
||||
|
||||
See [hindsight-docs/docs/cookbook/](./hindsight-docs/docs/cookbook/) for detailed guides:
|
||||
|
||||
- **Per-User Memory**: One bank per user, simplest pattern
|
||||
- **Support Agent + Shared Knowledge**: User bank + shared docs bank, client orchestrates
|
||||
|
||||
## Developer Guide
|
||||
|
||||
### Running the API Server
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
./scripts/dev/start-api.sh
|
||||
|
||||
# With options
|
||||
./scripts/dev/start-api.sh --reload --port 8888 --log-level debug
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# API tests
|
||||
cd hindsight-api
|
||||
uv run pytest tests/
|
||||
|
||||
# Specific test
|
||||
uv run pytest tests/test_http_api_integration.py -v
|
||||
```
|
||||
|
||||
### Generating OpenAPI Spec
|
||||
|
||||
After changing API endpoints, regenerate the OpenAPI spec and docs:
|
||||
|
||||
```bash
|
||||
./scripts/generate-openapi.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Generate `openapi.json` at project root
|
||||
2. Copy to `hindsight-docs/openapi.json`
|
||||
3. Regenerate API reference documentation
|
||||
|
||||
### Generating API Clients
|
||||
|
||||
After updating the OpenAPI spec, regenerate all clients:
|
||||
|
||||
```bash
|
||||
./scripts/generate-clients.sh
|
||||
```
|
||||
|
||||
This generates:
|
||||
- **Rust client**: `hindsight-clients/rust/` (via progenitor in build.rs)
|
||||
- **Python client**: `hindsight-clients/python/` (via openapi-generator Docker)
|
||||
- **TypeScript client**: `hindsight-clients/typescript/` (via @hey-api/openapi-ts)
|
||||
|
||||
Note: The maintained wrapper `hindsight_client.py` and `README.md` are preserved during regeneration.
|
||||
|
||||
### Running the Documentation Site
|
||||
|
||||
```bash
|
||||
./scripts/dev/start-docs.sh
|
||||
```
|
||||
|
||||
### Running the Control Plane
|
||||
|
||||
```bash
|
||||
./scripts/dev/start-control-plane.sh
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### Python (hindsight-api)
|
||||
- Use `uv` for package management
|
||||
- Async throughout (asyncpg, async FastAPI endpoints)
|
||||
- Pydantic models for request/response validation
|
||||
- No py files at project root - maintain clean directory structure
|
||||
|
||||
### TypeScript (control-plane, clients)
|
||||
- Next.js with App Router for control plane
|
||||
- Tailwind CSS with shadcn/ui components
|
||||
|
||||
### Rust (CLI)
|
||||
- Async with tokio
|
||||
- reqwest for HTTP client
|
||||
- progenitor for API client generation
|
||||
|
||||
## Database
|
||||
|
||||
- PostgreSQL with pgvector extension
|
||||
- Schema managed via Alembic migrations in `hindsight-api/alembic/`, db migrations happen during api startup, no manual commands
|
||||
- Key tables: `banks`, `memory_units`, `documents`, `entities`, `entity_links`
|
||||
|
||||
# Branding
|
||||
## Colors
|
||||
- Primary: gradient from #0074d9 to #009296
|
||||
|
||||
|
||||
@@ -199,38 +199,6 @@ When adding or modifying parameters in the dataplane API (hindsight-api), you mu
|
||||
- Pydantic models for request/response
|
||||
- Ruff for linting (line-length 120)
|
||||
- No Python files at project root - maintain clean directory structure
|
||||
- **Never use multi-item tuple return values** - prefer dataclass or Pydantic model for structured returns
|
||||
|
||||
### Type Safety with Pydantic Models
|
||||
**NEVER use raw `dict` types for structured data.** Always use Pydantic models:
|
||||
- Use Pydantic `BaseModel` for all data structures passed between functions
|
||||
- Add `@field_validator` for type coercion (e.g., ensuring datetimes are timezone-aware)
|
||||
- Avoid `dict.get()` patterns - use typed model attributes instead
|
||||
- Parse external data (JSON, API responses) into Pydantic models at the boundary
|
||||
- This catches type errors at parse time, not deep in business logic
|
||||
|
||||
```python
|
||||
# BAD - error-prone dict access
|
||||
def process(data: dict) -> str:
|
||||
return data.get("name", "") # No validation, silent failures
|
||||
|
||||
# GOOD - typed and validated
|
||||
class UserData(BaseModel):
|
||||
name: str
|
||||
created_at: datetime
|
||||
|
||||
@field_validator("created_at", mode="before")
|
||||
@classmethod
|
||||
def ensure_tz_aware(cls, v):
|
||||
if isinstance(v, str):
|
||||
v = datetime.fromisoformat(v.replace("Z", "+00:00"))
|
||||
if v.tzinfo is None:
|
||||
return v.replace(tzinfo=timezone.utc)
|
||||
return v
|
||||
|
||||
def process(data: UserData) -> str:
|
||||
return data.name # Type-safe, validated at construction
|
||||
```
|
||||
|
||||
### TypeScript Style
|
||||
- Next.js App Router for control plane
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
"""mental_model_versions
|
||||
|
||||
Revision ID: j5e6f7g8h9i0
|
||||
Revises: i4d5e6f7g8h9
|
||||
Create Date: 2026-01-16 00:00:00.000000
|
||||
|
||||
This migration adds versioning support for mental models:
|
||||
1. Creates mental_model_versions table to store observation snapshots
|
||||
2. Adds version column to mental_models for tracking current version
|
||||
|
||||
This enables changelog/diff functionality for mental model observations.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "j5e6f7g8h9i0"
|
||||
down_revision: str | Sequence[str] | None = "i4d5e6f7g8h9"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _get_schema_prefix() -> str:
|
||||
"""Get schema prefix for table names (required for multi-tenant support)."""
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Create mental_model_versions table and add version tracking."""
|
||||
schema = _get_schema_prefix()
|
||||
|
||||
# Create mental_model_versions table for storing observation snapshots
|
||||
op.execute(f"""
|
||||
CREATE TABLE {schema}mental_model_versions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
mental_model_id VARCHAR(64) NOT NULL,
|
||||
bank_id VARCHAR(64) NOT NULL,
|
||||
version INT NOT NULL,
|
||||
observations JSONB NOT NULL DEFAULT '{{"observations": []}}'::jsonb,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
FOREIGN KEY (mental_model_id, bank_id)
|
||||
REFERENCES {schema}mental_models(id, bank_id) ON DELETE CASCADE,
|
||||
UNIQUE (mental_model_id, bank_id, version)
|
||||
)
|
||||
""")
|
||||
|
||||
# Index for efficient version queries (get latest, list versions)
|
||||
op.execute(f"""
|
||||
CREATE INDEX idx_mental_model_versions_lookup
|
||||
ON {schema}mental_model_versions(mental_model_id, bank_id, version DESC)
|
||||
""")
|
||||
|
||||
# Add version column to mental_models to track current version
|
||||
op.execute(f"""
|
||||
ALTER TABLE {schema}mental_models
|
||||
ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 0
|
||||
""")
|
||||
|
||||
# Migrate existing mental models: create version 1 for any that have observations
|
||||
op.execute(f"""
|
||||
INSERT INTO {schema}mental_model_versions (mental_model_id, bank_id, version, observations, created_at)
|
||||
SELECT id, bank_id, 1, observations, COALESCE(last_updated, created_at)
|
||||
FROM {schema}mental_models
|
||||
WHERE observations IS NOT NULL
|
||||
AND observations != '{{"observations": []}}'::jsonb
|
||||
AND (observations->'observations') IS NOT NULL
|
||||
AND jsonb_array_length(observations->'observations') > 0
|
||||
""")
|
||||
|
||||
# Update version to 1 for migrated mental models
|
||||
op.execute(f"""
|
||||
UPDATE {schema}mental_models
|
||||
SET version = 1
|
||||
WHERE observations IS NOT NULL
|
||||
AND observations != '{{"observations": []}}'::jsonb
|
||||
AND (observations->'observations') IS NOT NULL
|
||||
AND jsonb_array_length(observations->'observations') > 0
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Remove mental_model_versions table and version column."""
|
||||
schema = _get_schema_prefix()
|
||||
|
||||
# Drop index
|
||||
op.execute(f"DROP INDEX IF EXISTS {schema}idx_mental_model_versions_lookup")
|
||||
|
||||
# Drop versions table
|
||||
op.execute(f"DROP TABLE IF EXISTS {schema}mental_model_versions")
|
||||
|
||||
# Remove version column from mental_models
|
||||
op.execute(f"ALTER TABLE {schema}mental_models DROP COLUMN IF EXISTS version")
|
||||
@@ -1,58 +0,0 @@
|
||||
"""add_directive_subtype
|
||||
|
||||
Revision ID: k6f7g8h9i0j1
|
||||
Revises: j5e6f7g8h9i0
|
||||
Create Date: 2026-01-16 00:00:00.000000
|
||||
|
||||
This migration adds 'directive' to the mental_models subtype constraint.
|
||||
Directives are hard rules with user-provided observations that the reflect agent must follow.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "k6f7g8h9i0j1"
|
||||
down_revision: str | Sequence[str] | None = "j5e6f7g8h9i0"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _get_schema_prefix() -> str:
|
||||
"""Get schema prefix for table names (required for multi-tenant support)."""
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Add 'directive' to mental_models subtype constraint."""
|
||||
schema = _get_schema_prefix()
|
||||
|
||||
# Drop existing constraint
|
||||
op.execute(f"ALTER TABLE {schema}mental_models DROP CONSTRAINT IF EXISTS ck_mental_models_subtype")
|
||||
|
||||
# Create new constraint with 'directive' added
|
||||
op.execute(f"""
|
||||
ALTER TABLE {schema}mental_models
|
||||
ADD CONSTRAINT ck_mental_models_subtype
|
||||
CHECK (subtype IN ('structural', 'emergent', 'pinned', 'learned', 'directive'))
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Remove 'directive' from mental_models subtype constraint."""
|
||||
schema = _get_schema_prefix()
|
||||
|
||||
# First delete any directives (cannot downgrade if they exist)
|
||||
op.execute(f"DELETE FROM {schema}mental_models WHERE subtype = 'directive'")
|
||||
|
||||
# Drop constraint with directive
|
||||
op.execute(f"ALTER TABLE {schema}mental_models DROP CONSTRAINT IF EXISTS ck_mental_models_subtype")
|
||||
|
||||
# Recreate original constraint without directive
|
||||
op.execute(f"""
|
||||
ALTER TABLE {schema}mental_models
|
||||
ADD CONSTRAINT ck_mental_models_subtype
|
||||
CHECK (subtype IN ('structural', 'emergent', 'pinned', 'learned'))
|
||||
""")
|
||||
@@ -36,7 +36,6 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from hindsight_api import MemoryEngine
|
||||
from hindsight_api.engine.db_utils import acquire_with_retry
|
||||
from hindsight_api.engine.memory_engine import Budget, fq_table
|
||||
from hindsight_api.engine.reflect.observations import Observation
|
||||
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES, TokenUsage
|
||||
from hindsight_api.engine.search.tags import TagsMatch
|
||||
from hindsight_api.extensions import HttpExtension, OperationValidationError, load_extension
|
||||
@@ -560,10 +559,9 @@ class ReflectMentalModel(BaseModel):
|
||||
id: str = Field(description="Mental model ID")
|
||||
name: str = Field(description="Mental model name")
|
||||
type: str = Field(description="Mental model type: entity, concept, event")
|
||||
subtype: str = Field(description="Mental model subtype: structural, emergent, learned, directive")
|
||||
observations: list[str] | None = Field(
|
||||
default=None, description="Observations for directive mental models (subtype='directive')"
|
||||
)
|
||||
subtype: str = Field(description="Mental model subtype: structural, emergent, learned")
|
||||
description: str = Field(description="Brief description")
|
||||
summary: str | None = Field(default=None, description="Full summary (when looked up in detail)")
|
||||
|
||||
|
||||
class ReflectBasedOn(BaseModel):
|
||||
@@ -580,10 +578,6 @@ class ReflectTrace(BaseModel):
|
||||
|
||||
tool_calls: list[ReflectToolCall] = Field(default_factory=list, description="Tool calls made during reflection")
|
||||
llm_calls: list[ReflectLLMCall] = Field(default_factory=list, description="LLM calls made during reflection")
|
||||
mental_models: list[ReflectMentalModel] = Field(
|
||||
default_factory=list,
|
||||
description="Mental models used during reflection (includes directives with subtype='directive')",
|
||||
)
|
||||
|
||||
|
||||
class CreatedMentalModel(BaseModel):
|
||||
@@ -1051,40 +1045,12 @@ class BankStatsResponse(BaseModel):
|
||||
# Mental Model models
|
||||
|
||||
|
||||
class ObservationEvidenceResponse(BaseModel):
|
||||
"""A single piece of evidence supporting an observation."""
|
||||
|
||||
memory_id: str = Field(description="ID of the memory unit this evidence comes from")
|
||||
quote: str = Field(description="Exact quote from the memory supporting the observation")
|
||||
relevance: str = Field(description="Brief explanation of how this quote supports the observation")
|
||||
timestamp: str = Field(description="When the source memory was created (ISO format)")
|
||||
|
||||
|
||||
class MentalModelObservationResponse(BaseModel):
|
||||
"""An observation within a mental model with its supporting evidence."""
|
||||
"""An observation within a mental model with its supporting memories."""
|
||||
|
||||
title: str = Field(description="Short summary title for the observation")
|
||||
content: str = Field(description="The observation content - detailed explanation")
|
||||
evidence: list[ObservationEvidenceResponse] = Field(
|
||||
default_factory=list, description="Supporting evidence with quotes"
|
||||
)
|
||||
created_at: str = Field(description="When this observation was first created (ISO format)")
|
||||
trend: str = Field(description="Computed trend: stable, strengthening, weakening, new, stale")
|
||||
evidence_count: int = Field(description="Number of evidence items supporting this observation")
|
||||
evidence_span: dict = Field(description="Time span of evidence: {from: iso_date, to: iso_date}")
|
||||
|
||||
|
||||
class MentalModelFreshnessResponse(BaseModel):
|
||||
"""Freshness information for a mental model."""
|
||||
|
||||
is_up_to_date: bool = Field(description="Whether the model has been refreshed since the last memory was added")
|
||||
last_refresh_at: str | None = Field(description="When the model was last refreshed (ISO format)")
|
||||
memories_since_refresh: int = Field(description="Number of memories added since last refresh")
|
||||
reasons: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Reasons why the model needs refresh (empty if up to date). "
|
||||
"Possible values: never_refreshed, new_memories, mission_changed, disposition_changed, directives_changed",
|
||||
)
|
||||
title: str = Field(description="Observation header (empty for intro)")
|
||||
text: str = Field(description="Observation content")
|
||||
based_on: list[str] = Field(default_factory=list, description="Memory IDs supporting this observation")
|
||||
|
||||
|
||||
class MentalModelResponse(BaseModel):
|
||||
@@ -1098,36 +1064,11 @@ class MentalModelResponse(BaseModel):
|
||||
"subtype": "structural",
|
||||
"name": "Team Structure",
|
||||
"description": "Who's on the team and their roles",
|
||||
"observations": [
|
||||
{
|
||||
"title": "Prefers async communication",
|
||||
"content": "The team prefers async communication over synchronous meetings",
|
||||
"evidence": [
|
||||
{
|
||||
"memory_id": "uuid1",
|
||||
"quote": "I prefer Slack over meetings",
|
||||
"relevance": "Shows async preference",
|
||||
"timestamp": "2024-01-10T08:00:00Z",
|
||||
}
|
||||
],
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"trend": "stable",
|
||||
"evidence_count": 1,
|
||||
"evidence_span": {"from": "2024-01-10T08:00:00Z", "to": "2024-01-10T08:00:00Z"},
|
||||
}
|
||||
],
|
||||
"version": 1,
|
||||
"observations": [{"title": "Overview", "text": "The team consists of...", "based_on": ["uuid1"]}],
|
||||
"entity_id": None,
|
||||
"links": [],
|
||||
"tags": ["project-x"],
|
||||
"last_updated": "2024-01-15T10:30:00Z",
|
||||
"last_refresh_at": "2024-01-15T10:30:00Z",
|
||||
"freshness": {
|
||||
"is_up_to_date": True,
|
||||
"last_refresh_at": "2024-01-15T10:30:00Z",
|
||||
"memories_since_refresh": 0,
|
||||
"reasons": [],
|
||||
},
|
||||
"created_at": "2024-01-10T08:00:00Z",
|
||||
}
|
||||
}
|
||||
@@ -1141,15 +1082,10 @@ class MentalModelResponse(BaseModel):
|
||||
observations: list[MentalModelObservationResponse] = Field(
|
||||
default_factory=list, description="Structured observations with per-observation fact attribution"
|
||||
)
|
||||
version: int = Field(default=0, description="Version number of the mental model observations")
|
||||
entity_id: str | None = None
|
||||
links: list[str] = []
|
||||
tags: list[str] = []
|
||||
last_updated: str | None = None
|
||||
last_refresh_at: str | None = Field(default=None, description="When observations were last refreshed (ISO format)")
|
||||
freshness: MentalModelFreshnessResponse | None = Field(
|
||||
default=None, description="Freshness info (null for directive subtypes which don't need refresh)"
|
||||
)
|
||||
created_at: str
|
||||
|
||||
|
||||
@@ -1159,39 +1095,6 @@ class MentalModelListResponse(BaseModel):
|
||||
items: list[MentalModelResponse]
|
||||
|
||||
|
||||
def _observation_to_response(obs: Observation) -> MentalModelObservationResponse:
|
||||
"""Convert internal Observation model to API response model."""
|
||||
return MentalModelObservationResponse(
|
||||
title=obs.title,
|
||||
content=obs.content,
|
||||
evidence=[
|
||||
ObservationEvidenceResponse(
|
||||
memory_id=ev.memory_id,
|
||||
quote=ev.quote,
|
||||
relevance=ev.relevance,
|
||||
timestamp=ev.timestamp.isoformat(),
|
||||
)
|
||||
for ev in obs.evidence
|
||||
],
|
||||
created_at=obs.created_at.isoformat(),
|
||||
trend=obs.trend.value,
|
||||
evidence_count=obs.evidence_count,
|
||||
evidence_span=obs.evidence_span,
|
||||
)
|
||||
|
||||
|
||||
def _prepare_mental_model_response(model: dict[str, Any]) -> MentalModelResponse:
|
||||
"""Convert internal mental model dict to API response model.
|
||||
|
||||
Handles conversion of Observation models to MentalModelObservationResponse.
|
||||
"""
|
||||
observations = model.get("observations", [])
|
||||
converted_observations = [
|
||||
_observation_to_response(obs) if isinstance(obs, Observation) else obs for obs in observations
|
||||
]
|
||||
return MentalModelResponse(**{**model, "observations": converted_observations})
|
||||
|
||||
|
||||
class RefreshMentalModelsRequest(BaseModel):
|
||||
"""Request model for refresh mental models endpoint."""
|
||||
|
||||
@@ -1204,63 +1107,24 @@ class RefreshMentalModelsRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class ObservationInput(BaseModel):
|
||||
"""Input model for a single observation."""
|
||||
|
||||
title: str = Field(description="Short title/header for the observation")
|
||||
content: str = Field(description="Content of the observation")
|
||||
|
||||
|
||||
class CreateMentalModelRequest(BaseModel):
|
||||
"""Request model for creating a mental model."""
|
||||
"""Request model for creating a pinned mental model."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"examples": [
|
||||
{
|
||||
"name": "Product Roadmap",
|
||||
"description": "Key product priorities and upcoming features",
|
||||
"tags": ["project-x"],
|
||||
},
|
||||
{
|
||||
"name": "Meeting Rules",
|
||||
"description": "Rules about scheduling meetings",
|
||||
"subtype": "directive",
|
||||
"observations": [{"title": "Morning meetings", "content": "Never schedule meetings before 10am"}],
|
||||
},
|
||||
]
|
||||
"example": {
|
||||
"name": "Product Roadmap",
|
||||
"description": "Key product priorities and upcoming features",
|
||||
"tags": ["project-x"],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
name: str = Field(description="Human-readable name for the mental model")
|
||||
description: str = Field(description="One-liner description for quick scanning")
|
||||
subtype: str = Field(
|
||||
default="pinned",
|
||||
description="Type of mental model: 'pinned' (observations LLM-generated) or 'directive' (observations user-provided)",
|
||||
)
|
||||
observations: list[ObservationInput] | None = Field(
|
||||
default=None,
|
||||
description="For directives only: list of user-provided observations. Required when subtype='directive'.",
|
||||
)
|
||||
tags: list[str] = Field(default_factory=list, description="Tags for scoped visibility")
|
||||
|
||||
|
||||
class UpdateMentalModelRequest(BaseModel):
|
||||
"""Request model for updating a mental model."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"name": "Updated Name",
|
||||
"description": "Updated description with new rules",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
name: str | None = Field(default=None, description="New name for the mental model")
|
||||
description: str | None = Field(default=None, description="New description/rule text")
|
||||
|
||||
|
||||
class OperationResponse(BaseModel):
|
||||
"""Response model for a single async operation."""
|
||||
|
||||
@@ -1878,12 +1742,14 @@ def _register_routes(app: FastAPI):
|
||||
name=mm.name,
|
||||
type=mm.type,
|
||||
subtype=mm.subtype,
|
||||
description=mm.description,
|
||||
summary=mm.summary,
|
||||
)
|
||||
for mm in core_result.mental_models
|
||||
]
|
||||
based_on_result = ReflectBasedOn(memories=memories, mental_models=mental_models)
|
||||
|
||||
# Build trace (tool_calls + llm_calls + mental_models) if tool_calls is requested
|
||||
# Build trace (tool_calls + llm_calls) if tool_calls is requested
|
||||
trace_result: ReflectTrace | None = None
|
||||
if request.include.tool_calls is not None:
|
||||
include_output = request.include.tool_calls.output
|
||||
@@ -1898,24 +1764,7 @@ def _register_routes(app: FastAPI):
|
||||
for tc in core_result.tool_trace
|
||||
]
|
||||
llm_calls = [ReflectLLMCall(scope=lc.scope, duration_ms=lc.duration_ms) for lc in core_result.llm_trace]
|
||||
# Build map of directive observations by id
|
||||
directive_observations = {d.id: d.rules for d in core_result.directives_applied}
|
||||
# Include all mental models (including directives with subtype='directive')
|
||||
trace_mental_models = [
|
||||
ReflectMentalModel(
|
||||
id=mm.id,
|
||||
name=mm.name,
|
||||
type=mm.type,
|
||||
subtype=mm.subtype,
|
||||
observations=directive_observations.get(mm.id) if mm.subtype == "directive" else None,
|
||||
)
|
||||
for mm in core_result.mental_models
|
||||
]
|
||||
trace_result = ReflectTrace(
|
||||
tool_calls=tool_calls,
|
||||
llm_calls=llm_calls,
|
||||
mental_models=trace_mental_models,
|
||||
)
|
||||
trace_result = ReflectTrace(tool_calls=tool_calls, llm_calls=llm_calls)
|
||||
|
||||
# Build mental_models_created from tool trace (learn tool outputs)
|
||||
created_models: list[CreatedMentalModel] = []
|
||||
@@ -2227,46 +2076,7 @@ def _register_routes(app: FastAPI):
|
||||
tags_match=tags_match,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Add freshness to each model (skip for directives)
|
||||
# Get data needed for freshness computation (once for all models)
|
||||
from hindsight_api.engine.reflect.mental_model_reflect import (
|
||||
BankProfile,
|
||||
DirectiveMentalModel,
|
||||
check_needs_refresh,
|
||||
)
|
||||
|
||||
total_memories = await app.state.memory._count_memories_since(bank_id, None)
|
||||
bank_profile_dict = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
|
||||
# Convert to typed models at the boundary
|
||||
bank_profile = BankProfile.model_validate(bank_profile_dict)
|
||||
directives = [DirectiveMentalModel.model_validate(m) for m in models if m.get("subtype") == "directive"]
|
||||
|
||||
for model in models:
|
||||
if model.get("subtype") != "directive":
|
||||
last_refresh_at = model.get("last_refresh_at")
|
||||
memories_since = await app.state.memory._count_memories_since(bank_id, last_refresh_at)
|
||||
|
||||
# Use check_needs_refresh to get reasons
|
||||
stored_refresh_state = model.get("refresh_state")
|
||||
refresh_check = check_needs_refresh(
|
||||
stored_state=stored_refresh_state,
|
||||
current_memories_count=total_memories,
|
||||
bank_profile=bank_profile,
|
||||
directives=directives,
|
||||
)
|
||||
|
||||
model["freshness"] = {
|
||||
"is_up_to_date": not refresh_check.needs_refresh,
|
||||
"last_refresh_at": last_refresh_at,
|
||||
"memories_since_refresh": memories_since,
|
||||
"reasons": refresh_check.reasons,
|
||||
}
|
||||
else:
|
||||
model["freshness"] = None
|
||||
|
||||
return MentalModelListResponse(items=[_prepare_mental_model_response(m) for m in models])
|
||||
return MentalModelListResponse(items=[MentalModelResponse(**m) for m in models])
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -2280,11 +2090,7 @@ def _register_routes(app: FastAPI):
|
||||
"/v1/default/banks/{bank_id}/mental-models",
|
||||
response_model=MentalModelResponse,
|
||||
summary="Create mental model",
|
||||
description=(
|
||||
"Create a mental model. Supports two subtypes:\n"
|
||||
"- 'pinned' (default): User-defined topic, observations are LLM-generated on refresh\n"
|
||||
"- 'directive': User-defined hard rules, observations are provided at creation and never regenerated"
|
||||
),
|
||||
description="Create a pinned mental model. Pinned models are user-defined and persist across refreshes.",
|
||||
operation_id="create_mental_model",
|
||||
tags=["Mental Models"],
|
||||
)
|
||||
@@ -2293,23 +2099,16 @@ def _register_routes(app: FastAPI):
|
||||
body: CreateMentalModelRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Create a mental model (pinned or directive)."""
|
||||
"""Create a pinned mental model."""
|
||||
try:
|
||||
# Convert observations to list of dicts if provided
|
||||
observations_list = None
|
||||
if body.observations:
|
||||
observations_list = [{"title": obs.title, "content": obs.content} for obs in body.observations]
|
||||
|
||||
model = await app.state.memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name=body.name,
|
||||
description=body.description,
|
||||
subtype=body.subtype,
|
||||
observations=observations_list,
|
||||
tags=body.tags,
|
||||
request_context=request_context,
|
||||
)
|
||||
return _prepare_mental_model_response(model)
|
||||
return MentalModelResponse(**model)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except (AuthenticationError, HTTPException):
|
||||
@@ -2343,47 +2142,7 @@ def _register_routes(app: FastAPI):
|
||||
)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=404, detail=f"Mental model '{model_id}' not found")
|
||||
|
||||
# Compute freshness for non-directive models
|
||||
if model.get("subtype") != "directive":
|
||||
from hindsight_api.engine.reflect.mental_model_reflect import (
|
||||
BankProfile,
|
||||
DirectiveMentalModel,
|
||||
check_needs_refresh,
|
||||
)
|
||||
|
||||
last_refresh_at = model.get("last_refresh_at")
|
||||
total_memories = await app.state.memory._count_memories_since(bank_id, None)
|
||||
memories_since = await app.state.memory._count_memories_since(bank_id, last_refresh_at)
|
||||
bank_profile_dict = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
directives_dicts = await app.state.memory.list_mental_models(
|
||||
bank_id, subtype="directive", request_context=request_context
|
||||
)
|
||||
|
||||
# Convert to typed models at the boundary
|
||||
bank_profile = BankProfile.model_validate(bank_profile_dict)
|
||||
directives = [DirectiveMentalModel.model_validate(d) for d in directives_dicts]
|
||||
|
||||
# Use check_needs_refresh to get reasons
|
||||
stored_refresh_state = model.get("refresh_state")
|
||||
refresh_check = check_needs_refresh(
|
||||
stored_state=stored_refresh_state,
|
||||
current_memories_count=total_memories,
|
||||
bank_profile=bank_profile,
|
||||
directives=directives,
|
||||
)
|
||||
|
||||
model["freshness"] = {
|
||||
"is_up_to_date": not refresh_check.needs_refresh,
|
||||
"last_refresh_at": last_refresh_at,
|
||||
"memories_since_refresh": memories_since,
|
||||
"reasons": refresh_check.reasons,
|
||||
}
|
||||
else:
|
||||
# Directives don't need freshness - they're static
|
||||
model["freshness"] = None
|
||||
|
||||
return _prepare_mental_model_response(model)
|
||||
return MentalModelResponse(**model)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -2468,61 +2227,23 @@ def _register_routes(app: FastAPI):
|
||||
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/mental-models/{model_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.patch(
|
||||
"/v1/default/banks/{bank_id}/mental-models/{model_id}",
|
||||
response_model=MentalModelResponse,
|
||||
summary="Update mental model",
|
||||
description="Update a mental model's name and/or description. Useful for editing directives.",
|
||||
operation_id="update_mental_model",
|
||||
tags=["Mental Models"],
|
||||
)
|
||||
async def api_update_mental_model(
|
||||
bank_id: str,
|
||||
model_id: str,
|
||||
body: UpdateMentalModelRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Update a mental model's name and/or description."""
|
||||
try:
|
||||
if body.name is None and body.description is None:
|
||||
raise HTTPException(status_code=400, detail="At least one of 'name' or 'description' must be provided")
|
||||
|
||||
updated = await app.state.memory.update_mental_model(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
name=body.name,
|
||||
description=body.description,
|
||||
request_context=request_context,
|
||||
)
|
||||
if not updated:
|
||||
raise HTTPException(status_code=404, detail=f"Mental model '{model_id}' not found")
|
||||
return _prepare_mental_model_response(updated)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}/mental-models/{model_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post(
|
||||
"/v1/default/banks/{bank_id}/mental-models/{model_id}/refresh",
|
||||
"/v1/default/banks/{bank_id}/mental-models/{model_id}/generate",
|
||||
response_model=AsyncOperationSubmitResponse,
|
||||
summary="Refresh mental model content (async)",
|
||||
description="Submit a background job to refresh content for a specific mental model. "
|
||||
"This is useful for newly created learned models or to refresh content for any model.",
|
||||
operation_id="refresh_mental_model",
|
||||
summary="Generate mental model content (async)",
|
||||
description="Submit a background job to generate/refresh content for a specific mental model. "
|
||||
"This is useful for newly created learned models or to regenerate content for any model.",
|
||||
operation_id="generate_mental_model",
|
||||
tags=["Mental Models"],
|
||||
)
|
||||
async def api_refresh_mental_model(
|
||||
async def api_generate_mental_model(
|
||||
bank_id: str,
|
||||
model_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Refresh content for a specific mental model."""
|
||||
"""Generate content for a specific mental model."""
|
||||
try:
|
||||
result = await app.state.memory.refresh_mental_model_async(
|
||||
result = await app.state.memory.generate_mental_model_async(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
request_context=request_context,
|
||||
@@ -2539,74 +2260,7 @@ def _register_routes(app: FastAPI):
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in POST /v1/default/banks/{bank_id}/mental-models/{model_id}/refresh: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/mental-models/{model_id}/versions",
|
||||
summary="List mental model version history",
|
||||
description="List all saved versions of a mental model's observations, ordered by version descending.",
|
||||
operation_id="list_mental_model_versions",
|
||||
tags=["Mental Models"],
|
||||
)
|
||||
async def api_list_mental_model_versions(
|
||||
bank_id: str,
|
||||
model_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""List version history for a mental model."""
|
||||
try:
|
||||
versions = await app.state.memory.get_mental_model_versions(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
return {"versions": versions}
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in GET /v1/default/banks/{bank_id}/mental-models/{model_id}/versions: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/mental-models/{model_id}/versions/{version}",
|
||||
summary="Get specific mental model version",
|
||||
description="Get observations from a specific version of a mental model.",
|
||||
operation_id="get_mental_model_version",
|
||||
tags=["Mental Models"],
|
||||
)
|
||||
async def api_get_mental_model_version(
|
||||
bank_id: str,
|
||||
model_id: str,
|
||||
version: int,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Get a specific version of a mental model."""
|
||||
try:
|
||||
version_data = await app.state.memory.get_mental_model_version(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
version=version,
|
||||
request_context=request_context,
|
||||
)
|
||||
if not version_data:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Version {version} not found for mental model '{model_id}'",
|
||||
)
|
||||
return version_data
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(
|
||||
f"Error in GET /v1/default/banks/{bank_id}/mental-models/{model_id}/versions/{version}: {error_detail}"
|
||||
)
|
||||
logger.error(f"Error in POST /v1/default/banks/{bank_id}/mental-models/{model_id}/generate: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
|
||||
@@ -4,12 +4,9 @@ Centralized configuration for Hindsight API.
|
||||
All environment variables and their defaults are defined here.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
|
||||
@@ -71,7 +68,6 @@ ENV_RERANKER_FLASHRANK_CACHE_DIR = "HINDSIGHT_API_RERANKER_FLASHRANK_CACHE_DIR"
|
||||
ENV_HOST = "HINDSIGHT_API_HOST"
|
||||
ENV_PORT = "HINDSIGHT_API_PORT"
|
||||
ENV_LOG_LEVEL = "HINDSIGHT_API_LOG_LEVEL"
|
||||
ENV_LOG_FORMAT = "HINDSIGHT_API_LOG_FORMAT"
|
||||
ENV_WORKERS = "HINDSIGHT_API_WORKERS"
|
||||
ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED"
|
||||
ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER"
|
||||
@@ -146,7 +142,6 @@ DEFAULT_RERANKER_LITELLM_MODEL = "cohere/rerank-english-v3.0"
|
||||
DEFAULT_HOST = "0.0.0.0"
|
||||
DEFAULT_PORT = 8888
|
||||
DEFAULT_LOG_LEVEL = "info"
|
||||
DEFAULT_LOG_FORMAT = "text" # Options: "text", "json"
|
||||
DEFAULT_WORKERS = 1
|
||||
DEFAULT_MCP_ENABLED = True
|
||||
DEFAULT_GRAPH_RETRIEVER = "link_expansion" # Options: "link_expansion", "mpfp", "bfs"
|
||||
@@ -209,36 +204,6 @@ Use this tool PROACTIVELY to:
|
||||
EMBEDDING_DIMENSION = DEFAULT_EMBEDDING_DIMENSION
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
"""JSON formatter for structured logging.
|
||||
|
||||
Outputs logs in JSON format with a 'severity' field that cloud logging
|
||||
systems (GCP, AWS CloudWatch, etc.) can parse to correctly categorize log levels.
|
||||
"""
|
||||
|
||||
SEVERITY_MAP = {
|
||||
logging.DEBUG: "DEBUG",
|
||||
logging.INFO: "INFO",
|
||||
logging.WARNING: "WARNING",
|
||||
logging.ERROR: "ERROR",
|
||||
logging.CRITICAL: "CRITICAL",
|
||||
}
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
log_entry = {
|
||||
"severity": self.SEVERITY_MAP.get(record.levelno, "DEFAULT"),
|
||||
"message": record.getMessage(),
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"logger": record.name,
|
||||
}
|
||||
|
||||
# Add exception info if present
|
||||
if record.exc_info:
|
||||
log_entry["exception"] = self.formatException(record.exc_info)
|
||||
|
||||
return json.dumps(log_entry)
|
||||
|
||||
|
||||
def _validate_extraction_mode(mode: str) -> str:
|
||||
"""Validate and normalize extraction mode."""
|
||||
mode_lower = mode.lower()
|
||||
@@ -297,7 +262,6 @@ class HindsightConfig:
|
||||
host: str
|
||||
port: int
|
||||
log_level: str
|
||||
log_format: str
|
||||
mcp_enabled: bool
|
||||
|
||||
# Recall
|
||||
@@ -381,7 +345,6 @@ class HindsightConfig:
|
||||
host=os.getenv(ENV_HOST, DEFAULT_HOST),
|
||||
port=int(os.getenv(ENV_PORT, DEFAULT_PORT)),
|
||||
log_level=os.getenv(ENV_LOG_LEVEL, DEFAULT_LOG_LEVEL),
|
||||
log_format=os.getenv(ENV_LOG_FORMAT, DEFAULT_LOG_FORMAT).lower(),
|
||||
mcp_enabled=os.getenv(ENV_MCP_ENABLED, str(DEFAULT_MCP_ENABLED)).lower() == "true",
|
||||
# Recall
|
||||
graph_retriever=os.getenv(ENV_GRAPH_RETRIEVER, DEFAULT_GRAPH_RETRIEVER),
|
||||
@@ -464,28 +427,12 @@ class HindsightConfig:
|
||||
return log_level_map.get(self.log_level.lower(), logging.INFO)
|
||||
|
||||
def configure_logging(self) -> None:
|
||||
"""Configure Python logging based on the log level and format.
|
||||
|
||||
When log_format is "json", outputs structured JSON logs with a severity
|
||||
field that GCP Cloud Logging can parse for proper log level categorization.
|
||||
"""
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(self.get_python_log_level())
|
||||
|
||||
# Remove existing handlers
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
# Create handler writing to stdout (GCP treats stderr as ERROR)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setLevel(self.get_python_log_level())
|
||||
|
||||
if self.log_format == "json":
|
||||
handler.setFormatter(JsonFormatter())
|
||||
else:
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s"))
|
||||
|
||||
root_logger.addHandler(handler)
|
||||
"""Configure Python logging based on the log level."""
|
||||
logging.basicConfig(
|
||||
level=self.get_python_log_level(),
|
||||
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
||||
force=True, # Override any existing configuration
|
||||
)
|
||||
|
||||
def log_config(self) -> None:
|
||||
"""Log the current configuration (without sensitive values)."""
|
||||
|
||||
@@ -136,12 +136,7 @@ class LocalSTCrossEncoder(CrossEncoderModel):
|
||||
# 3. Data transfer overhead to GPU outweighs compute benefit
|
||||
# 4. CPU inference is actually faster for this workload
|
||||
logger.info(f"Reranker: initializing local provider with model {self.model_name}")
|
||||
# Disable lazy loading (meta tensors) which causes issues with newer transformers/accelerate.
|
||||
# Setting low_cpu_mem_usage=False and device_map=None ensures tensors are fully materialized.
|
||||
self._model = CrossEncoder(
|
||||
self.model_name,
|
||||
model_kwargs={"low_cpu_mem_usage": False, "device_map": None},
|
||||
)
|
||||
self._model = CrossEncoder(self.model_name)
|
||||
|
||||
# Initialize shared executor (limited workers naturally limits concurrency)
|
||||
if LocalSTCrossEncoder._executor is None:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,7 @@ class MentalModelSubtype(str, Enum):
|
||||
STRUCTURAL = "structural" # Derived from mission, created upfront
|
||||
EMERGENT = "emergent" # Discovered from data patterns
|
||||
LEARNED = "learned" # Formed through reflection
|
||||
PINNED = "pinned" # User-defined topic, observations LLM-generated
|
||||
DIRECTIVE = "directive" # User-defined hard rules, observations user-provided
|
||||
PINNED = "pinned" # User-defined, persists across refreshes
|
||||
|
||||
|
||||
class MentalModel(BaseModel):
|
||||
|
||||
@@ -6,37 +6,12 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Literal
|
||||
|
||||
from .models import DirectiveInfo, LLMCall, MentalModelInput, ReflectAgentResult, ToolCall
|
||||
from .prompts import FINAL_SYSTEM_PROMPT, _extract_directive_rules, build_final_prompt, build_system_prompt_for_tools
|
||||
from .models import LLMCall, MentalModelInput, Observation, ReflectAgentResult, ToolCall
|
||||
from .prompts import FINAL_SYSTEM_PROMPT, build_final_prompt, build_system_prompt_for_tools
|
||||
from .tools_schema import get_reflect_tools
|
||||
|
||||
|
||||
def _build_directives_applied(directives: list[dict[str, Any]] | None) -> list[DirectiveInfo]:
|
||||
"""Build list of DirectiveInfo from directive mental models."""
|
||||
if not directives:
|
||||
return []
|
||||
|
||||
result = []
|
||||
for directive in directives:
|
||||
directive_id = directive.get("id", "")
|
||||
directive_name = directive.get("name", "")
|
||||
observations = directive.get("observations", [])
|
||||
|
||||
rules = []
|
||||
for obs in observations:
|
||||
# Support both Pydantic Observation objects and dicts
|
||||
if hasattr(obs, "content"):
|
||||
rules.append(obs.content)
|
||||
elif isinstance(obs, dict) and obs.get("content"):
|
||||
rules.append(obs["content"])
|
||||
|
||||
result.append(DirectiveInfo(id=directive_id, name=directive_name, rules=rules))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..llm_wrapper import LLMProvider
|
||||
from ..response_models import LLMToolCall
|
||||
@@ -161,7 +136,7 @@ async def run_reflect_agent(
|
||||
max_iterations: int = DEFAULT_MAX_ITERATIONS,
|
||||
max_tokens: int | None = None,
|
||||
response_schema: dict | None = None,
|
||||
directives: list[dict[str, Any]] | None = None,
|
||||
output_mode: Literal["answer", "observations"] = "answer",
|
||||
) -> ReflectAgentResult:
|
||||
"""
|
||||
Execute the reflect agent loop using native tool calling.
|
||||
@@ -183,7 +158,7 @@ async def run_reflect_agent(
|
||||
max_iterations: Maximum number of iterations before forcing response
|
||||
max_tokens: Maximum tokens for the final response
|
||||
response_schema: Optional JSON Schema for structured output in final response
|
||||
directives: Optional list of directive mental models to inject as hard rules
|
||||
output_mode: "answer" returns final text, "observations" returns structured observations
|
||||
|
||||
Returns:
|
||||
ReflectAgentResult with final answer and metadata
|
||||
@@ -192,17 +167,11 @@ async def run_reflect_agent(
|
||||
reflect_id = f"{bank_id[:8]}-{int(time.time() * 1000) % 100000}"
|
||||
start_time = time.time()
|
||||
|
||||
# Build directives_applied for the trace
|
||||
directives_applied = _build_directives_applied(directives)
|
||||
# Get tools for this agent
|
||||
tools = get_reflect_tools(enable_learn=enable_learn, output_mode=output_mode)
|
||||
|
||||
# Extract directive rules for tool schema (if any)
|
||||
directive_rules = _extract_directive_rules(directives) if directives else None
|
||||
|
||||
# Get tools for this agent (with directive compliance field if directives exist)
|
||||
tools = get_reflect_tools(enable_learn=enable_learn, directive_rules=directive_rules)
|
||||
|
||||
# Build initial messages (directives are injected into system prompt at START and END)
|
||||
system_prompt = build_system_prompt_for_tools(bank_profile, context, directives=directives)
|
||||
# Build initial messages
|
||||
system_prompt = build_system_prompt_for_tools(bank_profile, context, output_mode=output_mode)
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": query},
|
||||
@@ -220,43 +189,44 @@ async def run_reflect_agent(
|
||||
available_memory_ids: set[str] = set()
|
||||
available_model_ids: set[str] = set()
|
||||
|
||||
# Pre-fetch mental models so the agent always starts with this knowledge
|
||||
prefetch_start = time.time()
|
||||
models_result = await lookup_fn(None) # List all mental models
|
||||
prefetch_duration = int((time.time() - prefetch_start) * 1000)
|
||||
# In answer mode, pre-fetch mental models so the agent always starts with this knowledge
|
||||
if output_mode == "answer":
|
||||
prefetch_start = time.time()
|
||||
models_result = await lookup_fn(None) # List all mental models
|
||||
prefetch_duration = int((time.time() - prefetch_start) * 1000)
|
||||
|
||||
# Track available model IDs
|
||||
if isinstance(models_result, dict) and "models" in models_result:
|
||||
for model in models_result["models"]:
|
||||
if "id" in model:
|
||||
available_model_ids.add(model["id"])
|
||||
# Track available model IDs
|
||||
if isinstance(models_result, dict) and "models" in models_result:
|
||||
for model in models_result["models"]:
|
||||
if "id" in model:
|
||||
available_model_ids.add(model["id"])
|
||||
|
||||
# Add to context history for the agent
|
||||
context_history.append({"tool": "list_mental_models", "output": models_result})
|
||||
# Add to context history for the agent
|
||||
context_history.append({"tool": "list_mental_models", "output": models_result})
|
||||
|
||||
# Add to tool trace
|
||||
tool_trace.append(
|
||||
ToolCall(
|
||||
tool="list_mental_models",
|
||||
input={"tool": "list_mental_models"},
|
||||
output=models_result,
|
||||
duration_ms=prefetch_duration,
|
||||
iteration=0,
|
||||
# Add to tool trace
|
||||
tool_trace.append(
|
||||
ToolCall(
|
||||
tool="list_mental_models",
|
||||
input={"tool": "list_mental_models"},
|
||||
output=models_result,
|
||||
duration_ms=prefetch_duration,
|
||||
iteration=0,
|
||||
)
|
||||
)
|
||||
)
|
||||
tool_trace_summary.append(
|
||||
{
|
||||
"tool": "list_mental_models",
|
||||
"input_summary": "(prefetch)",
|
||||
"duration_ms": prefetch_duration,
|
||||
"output_chars": len(json.dumps(models_result, default=str)),
|
||||
}
|
||||
)
|
||||
total_tools_called += 1
|
||||
tool_trace_summary.append(
|
||||
{
|
||||
"tool": "list_mental_models",
|
||||
"input_summary": "(prefetch)",
|
||||
"duration_ms": prefetch_duration,
|
||||
"output_chars": len(json.dumps(models_result, default=str)),
|
||||
}
|
||||
)
|
||||
total_tools_called += 1
|
||||
|
||||
# Include in the user message so the agent sees it
|
||||
models_info = json.dumps(models_result, indent=2, default=str)
|
||||
messages[1]["content"] = f"{query}\n\n## Available Mental Models (pre-fetched)\n```json\n{models_info}\n```"
|
||||
# Include in the user message so the agent sees it
|
||||
models_info = json.dumps(models_result, indent=2, default=str)
|
||||
messages[1]["content"] = f"{query}\n\n## Available Mental Models (pre-fetched)\n```json\n{models_info}\n```"
|
||||
|
||||
def _get_llm_trace() -> list[LLMCall]:
|
||||
return [LLMCall(scope=c["scope"], duration_ms=c["duration_ms"]) for c in llm_trace]
|
||||
@@ -318,7 +288,6 @@ async def run_reflect_agent(
|
||||
mental_models_created=mental_models_created,
|
||||
tool_trace=tool_trace,
|
||||
llm_trace=_get_llm_trace(),
|
||||
directives_applied=directives_applied,
|
||||
)
|
||||
|
||||
# Call LLM with tools
|
||||
@@ -369,7 +338,6 @@ async def run_reflect_agent(
|
||||
mental_models_created=mental_models_created,
|
||||
tool_trace=tool_trace,
|
||||
llm_trace=_get_llm_trace(),
|
||||
directives_applied=directives_applied,
|
||||
)
|
||||
|
||||
# No tool calls - LLM wants to respond with text
|
||||
@@ -393,7 +361,6 @@ async def run_reflect_agent(
|
||||
mental_models_created=mental_models_created,
|
||||
tool_trace=tool_trace,
|
||||
llm_trace=_get_llm_trace(),
|
||||
directives_applied=directives_applied,
|
||||
)
|
||||
# Empty response, force final
|
||||
prompt = build_final_prompt(query, context_history, bank_profile, context)
|
||||
@@ -423,11 +390,10 @@ async def run_reflect_agent(
|
||||
mental_models_created=mental_models_created,
|
||||
tool_trace=tool_trace,
|
||||
llm_trace=_get_llm_trace(),
|
||||
directives_applied=directives_applied,
|
||||
)
|
||||
|
||||
# Check for done tool call (handle both 'done' and 'functions.done')
|
||||
done_call = next((tc for tc in result.tool_calls if tc.name == "done" or tc.name == "functions.done"), None)
|
||||
# Check for done tool call
|
||||
done_call = next((tc for tc in result.tool_calls if tc.name == "done"), None)
|
||||
if done_call:
|
||||
# Guardrail: Require evidence before done
|
||||
has_gathered_evidence = bool(available_memory_ids) or bool(available_model_ids)
|
||||
@@ -455,6 +421,7 @@ async def run_reflect_agent(
|
||||
# Process done tool
|
||||
return await _process_done_tool(
|
||||
done_call,
|
||||
output_mode,
|
||||
available_memory_ids,
|
||||
available_model_ids,
|
||||
iteration + 1,
|
||||
@@ -464,13 +431,12 @@ async def run_reflect_agent(
|
||||
_get_llm_trace(),
|
||||
_log_completion,
|
||||
reflect_id,
|
||||
directives_applied=directives_applied,
|
||||
llm_config=llm_config,
|
||||
response_schema=response_schema,
|
||||
)
|
||||
|
||||
# Execute other tools in parallel (exclude done and functions.done)
|
||||
other_tools = [tc for tc in result.tool_calls if tc.name not in ("done", "functions.done")]
|
||||
# Execute other tools in parallel
|
||||
other_tools = [tc for tc in result.tool_calls if tc.name != "done"]
|
||||
if other_tools:
|
||||
# Add assistant message with tool calls
|
||||
messages.append(
|
||||
@@ -568,7 +534,6 @@ async def run_reflect_agent(
|
||||
mental_models_created=mental_models_created,
|
||||
tool_trace=tool_trace,
|
||||
llm_trace=_get_llm_trace(),
|
||||
directives_applied=directives_applied,
|
||||
)
|
||||
|
||||
|
||||
@@ -586,6 +551,7 @@ def _tool_call_to_dict(tc: "LLMToolCall") -> dict[str, Any]:
|
||||
|
||||
async def _process_done_tool(
|
||||
done_call: "LLMToolCall",
|
||||
output_mode: str,
|
||||
available_memory_ids: set[str],
|
||||
available_model_ids: set[str],
|
||||
iterations: int,
|
||||
@@ -595,13 +561,60 @@ async def _process_done_tool(
|
||||
llm_trace: list[LLMCall],
|
||||
log_completion: Callable,
|
||||
reflect_id: str,
|
||||
directives_applied: list[DirectiveInfo],
|
||||
llm_config: "LLMProvider | None" = None,
|
||||
response_schema: dict | None = None,
|
||||
) -> ReflectAgentResult:
|
||||
"""Process the done tool call and return the result."""
|
||||
args = done_call.arguments
|
||||
|
||||
if output_mode == "observations" and "observations" in args:
|
||||
# Process observations - handle both list and nested {"observations": [...]} format
|
||||
observations: list[Observation] = []
|
||||
used_memory_ids: list[str] = []
|
||||
|
||||
obs_list = args["observations"]
|
||||
# Handle nested format where LLM outputs {"observations": [...]} instead of just [...]
|
||||
if isinstance(obs_list, dict) and "observations" in obs_list:
|
||||
obs_list = obs_list["observations"]
|
||||
|
||||
for obs_data in obs_list:
|
||||
validated_mids = []
|
||||
for mid in obs_data.get("memory_ids", []):
|
||||
if mid in available_memory_ids:
|
||||
validated_mids.append(mid)
|
||||
if mid not in used_memory_ids:
|
||||
used_memory_ids.append(mid)
|
||||
|
||||
observations.append(
|
||||
Observation(
|
||||
title=obs_data.get("title", ""),
|
||||
text=obs_data.get("text", ""),
|
||||
memory_ids=validated_mids,
|
||||
)
|
||||
)
|
||||
|
||||
# Build text from observations
|
||||
text_parts = []
|
||||
for obs in observations:
|
||||
if obs.title:
|
||||
text_parts.append(f"## {obs.title}\n{obs.text}")
|
||||
else:
|
||||
text_parts.append(obs.text)
|
||||
answer = "\n\n".join(text_parts)
|
||||
|
||||
log_completion(answer, iterations)
|
||||
return ReflectAgentResult(
|
||||
text=answer,
|
||||
observations=observations,
|
||||
iterations=iterations,
|
||||
tools_called=total_tools_called,
|
||||
mental_models_created=mental_models_created,
|
||||
tool_trace=tool_trace,
|
||||
llm_trace=llm_trace,
|
||||
used_memory_ids=used_memory_ids,
|
||||
)
|
||||
|
||||
# Default: answer mode
|
||||
answer = args.get("answer", "").strip()
|
||||
if not answer:
|
||||
answer = "No answer provided."
|
||||
@@ -626,7 +639,6 @@ async def _process_done_tool(
|
||||
llm_trace=llm_trace,
|
||||
used_memory_ids=used_memory_ids,
|
||||
used_model_ids=used_model_ids,
|
||||
directives_applied=directives_applied,
|
||||
)
|
||||
|
||||
|
||||
@@ -653,10 +665,6 @@ async def _execute_tool(
|
||||
learn_fn: Callable[[MentalModelInput], Awaitable[dict[str, Any]]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute a single tool by name."""
|
||||
# Normalize tool name - some LLMs return 'functions.done' instead of 'done'
|
||||
if tool_name.startswith("functions."):
|
||||
tool_name = tool_name[len("functions.") :]
|
||||
|
||||
if tool_name == "list_mental_models":
|
||||
return await lookup_fn(None)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -87,18 +87,21 @@ class LLMCall(BaseModel):
|
||||
duration_ms: int = Field(description="Execution time in milliseconds")
|
||||
|
||||
|
||||
class DirectiveInfo(BaseModel):
|
||||
"""Information about a directive that was applied during reflect."""
|
||||
class Observation(BaseModel):
|
||||
"""A single observation with supporting memories."""
|
||||
|
||||
id: str = Field(description="Directive mental model ID")
|
||||
name: str = Field(description="Directive name")
|
||||
rules: list[str] = Field(default_factory=list, description="Directive rules/observations that were applied")
|
||||
title: str = Field(description="Observation title/header")
|
||||
text: str = Field(description="Observation content")
|
||||
memory_ids: list[str] = Field(default_factory=list, description="Memory IDs supporting this observation")
|
||||
|
||||
|
||||
class ReflectAgentResult(BaseModel):
|
||||
"""Result from the reflect agent."""
|
||||
|
||||
text: str = Field(description="Final answer text")
|
||||
observations: list[Observation] = Field(
|
||||
default_factory=list, description="Structured observations (when output_mode=observations)"
|
||||
)
|
||||
structured_output: dict[str, Any] | None = Field(
|
||||
default=None, description="Structured output parsed according to provided response_schema"
|
||||
)
|
||||
@@ -109,6 +112,3 @@ class ReflectAgentResult(BaseModel):
|
||||
llm_trace: list[LLMCall] = Field(default_factory=list, description="Trace of all LLM calls made")
|
||||
used_memory_ids: list[str] = Field(default_factory=list, description="Validated memory IDs actually used in answer")
|
||||
used_model_ids: list[str] = Field(default_factory=list, description="Validated model IDs actually used in answer")
|
||||
directives_applied: list[DirectiveInfo] = Field(
|
||||
default_factory=list, description="Directive mental models that affected this reflection"
|
||||
)
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
"""
|
||||
Models and utilities for evidence-grounded observations with computed trends.
|
||||
|
||||
Observations are part of mental models and represent patterns/beliefs derived
|
||||
from memories. Each observation must be grounded in specific evidence (quotes)
|
||||
from memories, and trends are computed algorithmically from evidence timestamps.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field, computed_field, field_validator
|
||||
|
||||
|
||||
class Trend(str, Enum):
|
||||
"""Computed trend for an observation based on evidence timestamps.
|
||||
|
||||
Trends indicate how an observation's evidence is distributed over time:
|
||||
- STABLE: Evidence spread across time, continues to present
|
||||
- STRENGTHENING: More/denser evidence recently than before
|
||||
- WEAKENING: Evidence mostly old, sparse recently
|
||||
- NEW: All evidence within recent window
|
||||
- STALE: No evidence in recent window (may no longer apply)
|
||||
"""
|
||||
|
||||
STABLE = "stable"
|
||||
STRENGTHENING = "strengthening"
|
||||
WEAKENING = "weakening"
|
||||
NEW = "new"
|
||||
STALE = "stale"
|
||||
|
||||
|
||||
class ObservationEvidence(BaseModel):
|
||||
"""A single piece of evidence supporting an observation.
|
||||
|
||||
Each evidence item must include an exact quote from the source memory
|
||||
to ensure observations are grounded and verifiable.
|
||||
"""
|
||||
|
||||
memory_id: str = Field(description="ID of the memory unit this evidence comes from")
|
||||
quote: str = Field(description="Exact quote from the memory supporting the observation")
|
||||
relevance: str = Field(default="", description="Brief explanation of how this quote supports the observation")
|
||||
timestamp: datetime = Field(description="When the source memory was created")
|
||||
|
||||
@field_validator("timestamp", mode="before")
|
||||
@classmethod
|
||||
def ensure_timezone_aware(cls, v: datetime | str | None) -> datetime:
|
||||
"""Ensure timestamp is always timezone-aware UTC."""
|
||||
if v is None:
|
||||
return datetime.now(timezone.utc)
|
||||
if isinstance(v, str):
|
||||
# Parse ISO format string, handling 'Z' suffix
|
||||
v = datetime.fromisoformat(v.replace("Z", "+00:00"))
|
||||
if isinstance(v, datetime):
|
||||
if v.tzinfo is None:
|
||||
return v.replace(tzinfo=timezone.utc)
|
||||
return v
|
||||
raise ValueError(f"Invalid timestamp type: {type(v)}")
|
||||
|
||||
|
||||
class Observation(BaseModel):
|
||||
"""A single observation within a mental model.
|
||||
|
||||
Observations represent patterns, preferences, beliefs, or other insights
|
||||
derived from memories. Each observation must be grounded in evidence
|
||||
with exact quotes from source memories.
|
||||
"""
|
||||
|
||||
title: str = Field(description="Short summary title for the observation (5-10 words)")
|
||||
content: str = Field(description="The observation content - detailed explanation of what we believe to be true")
|
||||
evidence: list[ObservationEvidence] = Field(default_factory=list, description="Supporting evidence with quotes")
|
||||
created_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc), description="When this observation was first created"
|
||||
)
|
||||
|
||||
@field_validator("created_at", mode="before")
|
||||
@classmethod
|
||||
def ensure_created_at_timezone_aware(cls, v: datetime | str | None) -> datetime:
|
||||
"""Ensure created_at is always timezone-aware UTC."""
|
||||
if v is None:
|
||||
return datetime.now(timezone.utc)
|
||||
if isinstance(v, str):
|
||||
v = datetime.fromisoformat(v.replace("Z", "+00:00"))
|
||||
if isinstance(v, datetime):
|
||||
if v.tzinfo is None:
|
||||
return v.replace(tzinfo=timezone.utc)
|
||||
return v
|
||||
raise ValueError(f"Invalid created_at type: {type(v)}")
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def trend(self) -> Trend:
|
||||
"""Compute trend from evidence timestamps."""
|
||||
return compute_trend(self.evidence)
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def evidence_span(self) -> dict[str, str | None]:
|
||||
"""Get the time span covered by evidence."""
|
||||
if not self.evidence:
|
||||
return {"from": None, "to": None}
|
||||
timestamps = [e.timestamp for e in self.evidence]
|
||||
return {
|
||||
"from": min(timestamps).isoformat(),
|
||||
"to": max(timestamps).isoformat(),
|
||||
}
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def evidence_count(self) -> int:
|
||||
"""Number of evidence items supporting this observation."""
|
||||
return len(self.evidence)
|
||||
|
||||
|
||||
def compute_trend(
|
||||
evidence: list[ObservationEvidence],
|
||||
now: datetime | None = None,
|
||||
recent_days: int = 30,
|
||||
old_days: int = 90,
|
||||
) -> Trend:
|
||||
"""Compute the trend for an observation based on evidence timestamps.
|
||||
|
||||
The trend indicates how the evidence is distributed over time:
|
||||
- STABLE: Evidence spread across time, continues to present
|
||||
- STRENGTHENING: More evidence recently than historically
|
||||
- WEAKENING: Evidence mostly old, sparse recently
|
||||
- NEW: All evidence is recent (within recent_days)
|
||||
- STALE: No evidence in recent window
|
||||
|
||||
Args:
|
||||
evidence: List of evidence items with timestamps
|
||||
now: Reference time for calculations (defaults to current UTC time)
|
||||
recent_days: Number of days to consider "recent" (default 30)
|
||||
old_days: Number of days to consider "old" (default 90)
|
||||
|
||||
Returns:
|
||||
Computed Trend enum value
|
||||
"""
|
||||
if now is None:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Ensure now is timezone-aware
|
||||
if now.tzinfo is None:
|
||||
now = now.replace(tzinfo=timezone.utc)
|
||||
|
||||
if not evidence:
|
||||
return Trend.STALE
|
||||
|
||||
recent_cutoff = now - timedelta(days=recent_days)
|
||||
old_cutoff = now - timedelta(days=old_days)
|
||||
|
||||
# Normalize timestamps to UTC for comparison
|
||||
def normalize_ts(ts: datetime) -> datetime:
|
||||
if ts.tzinfo is None:
|
||||
return ts.replace(tzinfo=timezone.utc)
|
||||
return ts
|
||||
|
||||
recent = [e for e in evidence if normalize_ts(e.timestamp) > recent_cutoff]
|
||||
old = [e for e in evidence if normalize_ts(e.timestamp) < old_cutoff]
|
||||
middle = [e for e in evidence if old_cutoff <= normalize_ts(e.timestamp) <= recent_cutoff]
|
||||
|
||||
# No recent evidence = stale
|
||||
if not recent:
|
||||
return Trend.STALE
|
||||
|
||||
# All evidence is recent = new
|
||||
if not old and not middle:
|
||||
return Trend.NEW
|
||||
|
||||
# Compare density (evidence per day)
|
||||
recent_density = len(recent) / recent_days if recent_days > 0 else 0
|
||||
older_period = old_days - recent_days
|
||||
older_density = (len(old) + len(middle)) / older_period if older_period > 0 else 0
|
||||
|
||||
# Avoid division by zero
|
||||
if older_density == 0:
|
||||
return Trend.NEW
|
||||
|
||||
ratio = recent_density / older_density
|
||||
|
||||
if ratio > 1.5:
|
||||
return Trend.STRENGTHENING
|
||||
elif ratio < 0.5:
|
||||
return Trend.WEAKENING
|
||||
else:
|
||||
return Trend.STABLE
|
||||
|
||||
|
||||
class CandidateObservation(BaseModel):
|
||||
"""A candidate observation generated during the seed phase.
|
||||
|
||||
Candidates are preliminary observations that need evidence validation
|
||||
before becoming full observations.
|
||||
"""
|
||||
|
||||
content: str = Field(description="The proposed observation content")
|
||||
seed_memory_ids: list[str] = Field(default_factory=list, description="Memory IDs that inspired this candidate")
|
||||
|
||||
|
||||
class CandidateWithEvidence(BaseModel):
|
||||
"""A candidate observation with gathered supporting and contradicting evidence."""
|
||||
|
||||
candidate: CandidateObservation
|
||||
supporting_memories: list[dict] = Field(default_factory=list, description="Memories that support this observation")
|
||||
contradicting_memories: list[dict] = Field(
|
||||
default_factory=list, description="Memories that contradict this observation"
|
||||
)
|
||||
|
||||
|
||||
class MentalModelSnapshot(BaseModel):
|
||||
"""A versioned snapshot of a mental model's observations.
|
||||
|
||||
Used for tracking changes over time and enabling diff views.
|
||||
"""
|
||||
|
||||
version: int = Field(description="Version number (1-indexed)")
|
||||
observations: list[Observation] = Field(default_factory=list, description="Observations at this version")
|
||||
created_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc), description="When this version was created"
|
||||
)
|
||||
reflect_summary: str | None = Field(default=None, description="Summary of changes in this version")
|
||||
|
||||
|
||||
def verify_evidence_quotes(
|
||||
observation: Observation,
|
||||
memories: dict[str, str],
|
||||
) -> tuple[bool, list[str]]:
|
||||
"""Verify that all evidence quotes exist in the referenced memories.
|
||||
|
||||
Args:
|
||||
observation: The observation to verify
|
||||
memories: Dict mapping memory_id to memory content
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, list of error messages)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
for evidence in observation.evidence:
|
||||
memory_content = memories.get(evidence.memory_id)
|
||||
if memory_content is None:
|
||||
errors.append(f"Memory {evidence.memory_id} not found")
|
||||
continue
|
||||
|
||||
if evidence.quote not in memory_content:
|
||||
errors.append(f"Quote not found in memory {evidence.memory_id}: '{evidence.quote[:50]}...'")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
@@ -6,111 +6,10 @@ import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _extract_directive_rules(directives: list[dict[str, Any]]) -> list[str]:
|
||||
"""
|
||||
Extract directive rules as a list of strings.
|
||||
|
||||
Args:
|
||||
directives: List of directive mental models with observations
|
||||
|
||||
Returns:
|
||||
List of directive rule strings
|
||||
"""
|
||||
rules = []
|
||||
for directive in directives:
|
||||
directive_name = directive.get("name", "")
|
||||
observations = directive.get("observations", [])
|
||||
if observations:
|
||||
for obs in observations:
|
||||
# Support both Pydantic Observation objects and dicts
|
||||
if hasattr(obs, "title"):
|
||||
title = obs.title
|
||||
content = obs.content
|
||||
else:
|
||||
title = obs.get("title", "")
|
||||
content = obs.get("content", "")
|
||||
if title and content:
|
||||
rules.append(f"**{title}**: {content}")
|
||||
elif content:
|
||||
rules.append(content)
|
||||
elif directive_name:
|
||||
# Fallback to description if no observations
|
||||
desc = directive.get("description", "")
|
||||
if desc:
|
||||
rules.append(f"**{directive_name}**: {desc}")
|
||||
return rules
|
||||
|
||||
|
||||
def build_directives_section(directives: list[dict[str, Any]]) -> str:
|
||||
"""
|
||||
Build the directives section for the system prompt.
|
||||
|
||||
Directives are hard rules that MUST be followed in all responses.
|
||||
|
||||
Args:
|
||||
directives: List of directive mental models with observations
|
||||
"""
|
||||
if not directives:
|
||||
return ""
|
||||
|
||||
rules = _extract_directive_rules(directives)
|
||||
if not rules:
|
||||
return ""
|
||||
|
||||
parts = [
|
||||
"## DIRECTIVES (MANDATORY)",
|
||||
"These are hard rules you MUST follow in ALL responses:",
|
||||
"",
|
||||
]
|
||||
|
||||
for rule in rules:
|
||||
parts.append(f"- {rule}")
|
||||
|
||||
parts.extend(
|
||||
[
|
||||
"",
|
||||
"NEVER violate these directives, even if other context suggests otherwise.",
|
||||
"IMPORTANT: Do NOT explain or justify how you handled directives in your answer. Just follow them silently.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def build_directives_reminder(directives: list[dict[str, Any]]) -> str:
|
||||
"""
|
||||
Build a reminder section for directives to place at the end of the prompt.
|
||||
|
||||
Args:
|
||||
directives: List of directive mental models with observations
|
||||
"""
|
||||
if not directives:
|
||||
return ""
|
||||
|
||||
rules = _extract_directive_rules(directives)
|
||||
if not rules:
|
||||
return ""
|
||||
|
||||
parts = [
|
||||
"",
|
||||
"## REMINDER: MANDATORY DIRECTIVES",
|
||||
"Before responding, ensure your answer complies with ALL of these directives:",
|
||||
"",
|
||||
]
|
||||
|
||||
for i, rule in enumerate(rules, 1):
|
||||
parts.append(f"{i}. {rule}")
|
||||
|
||||
parts.append("")
|
||||
parts.append("Your response will be REJECTED if it violates any directive above.")
|
||||
parts.append("Do NOT include any commentary about how you handled directives - just follow them.")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def build_system_prompt_for_tools(
|
||||
bank_profile: dict[str, Any],
|
||||
context: str | None = None,
|
||||
directives: list[dict[str, Any]] | None = None,
|
||||
output_mode: str = "answer",
|
||||
) -> str:
|
||||
"""
|
||||
Build the system prompt for tool-calling reflect agent.
|
||||
@@ -120,90 +19,128 @@ def build_system_prompt_for_tools(
|
||||
Args:
|
||||
bank_profile: Bank profile with name and mission
|
||||
context: Optional additional context
|
||||
directives: Optional list of directive mental models to inject as hard rules
|
||||
output_mode: "answer" for plain text response, "observations" for structured observations
|
||||
"""
|
||||
name = bank_profile.get("name", "Assistant")
|
||||
mission = bank_profile.get("mission", "")
|
||||
|
||||
no_info_rule = (
|
||||
"- Only say 'I don't have information' AFTER trying list_mental_models AND recall with no relevant results"
|
||||
)
|
||||
# Build critical rules based on mode
|
||||
if output_mode == "observations":
|
||||
no_info_rule = "- Only say 'I don't have information' AFTER trying recall with no relevant results"
|
||||
else:
|
||||
no_info_rule = (
|
||||
"- Only say 'I don't have information' AFTER trying list_mental_models AND recall with no relevant results"
|
||||
)
|
||||
|
||||
parts = []
|
||||
parts = [
|
||||
"You are a reflection agent that answers questions by reasoning over retrieved memories.",
|
||||
"",
|
||||
"## CRITICAL RULES",
|
||||
"- You must NEVER fabricate information that has no basis in retrieved data",
|
||||
"- You SHOULD synthesize, infer, and reason from the retrieved memories",
|
||||
"- You MUST call recall() before saying you don't have information",
|
||||
no_info_rule,
|
||||
"",
|
||||
"## How to Reason",
|
||||
"- If memories mention someone did an activity, you can infer they likely enjoyed it",
|
||||
"- Synthesize a coherent narrative from related memories",
|
||||
"- Be a thoughtful interpreter, not just a literal repeater",
|
||||
"- When the exact answer isn't stated, use what IS stated to give the best answer",
|
||||
"",
|
||||
"## Query Strategy (IMPORTANT)",
|
||||
"recall() uses semantic search. NEVER just echo the user's question - decompose it into targeted searches:",
|
||||
"",
|
||||
"BAD: User asks 'recurring lesson themes between students' → recall('recurring lesson themes between students')",
|
||||
"GOOD: Break it down into component searches:",
|
||||
" 1. recall('lessons') - find all lesson-related memories",
|
||||
" 2. recall('teaching sessions') - alternative phrasing",
|
||||
" 3. recall('student progress') - find student-related memories",
|
||||
" 4. recall('topics taught') - find subject matter",
|
||||
"",
|
||||
"Think: What ENTITIES and CONCEPTS does this question involve? Search for each separately.",
|
||||
"- Questions about patterns → search for the individual instances first",
|
||||
"- Questions comparing things → search for each thing separately",
|
||||
"- Questions about relationships → search for each party involved",
|
||||
"",
|
||||
"## Workflow",
|
||||
]
|
||||
|
||||
# Inject directives at the VERY START for maximum prominence
|
||||
if directives:
|
||||
parts.append(build_directives_section(directives))
|
||||
|
||||
parts.extend(
|
||||
[
|
||||
"You are a reflection agent that answers questions by reasoning over retrieved memories.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
parts.extend(
|
||||
[
|
||||
"## CRITICAL RULES",
|
||||
"- You must NEVER fabricate information that has no basis in retrieved data",
|
||||
"- You SHOULD synthesize, infer, and reason from the retrieved memories",
|
||||
"- You MUST call recall() before saying you don't have information",
|
||||
no_info_rule,
|
||||
"",
|
||||
"## How to Reason",
|
||||
"- If memories mention someone did an activity, you can infer they likely enjoyed it",
|
||||
"- Synthesize a coherent narrative from related memories",
|
||||
"- Be a thoughtful interpreter, not just a literal repeater",
|
||||
"- When the exact answer isn't stated, use what IS stated to give the best answer",
|
||||
"",
|
||||
"## Query Strategy (IMPORTANT)",
|
||||
"recall() uses semantic search. NEVER just echo the user's question - decompose it into targeted searches:",
|
||||
"",
|
||||
"BAD: User asks 'recurring lesson themes between students' → recall('recurring lesson themes between students')",
|
||||
"GOOD: Break it down into component searches:",
|
||||
" 1. recall('lessons') - find all lesson-related memories",
|
||||
" 2. recall('teaching sessions') - alternative phrasing",
|
||||
" 3. recall('student progress') - find student-related memories",
|
||||
" 4. recall('topics taught') - find subject matter",
|
||||
"",
|
||||
"Think: What ENTITIES and CONCEPTS does this question involve? Search for each separately.",
|
||||
"- Questions about patterns → search for the individual instances first",
|
||||
"- Questions comparing things → search for each thing separately",
|
||||
"- Questions about relationships → search for each party involved",
|
||||
"",
|
||||
"## Workflow",
|
||||
]
|
||||
)
|
||||
|
||||
# Answer mode: include mental model lookup in workflow
|
||||
parts.extend(
|
||||
[
|
||||
"1. Review the pre-fetched mental models for relevant synthesized knowledge",
|
||||
"2. If relevant, call get_mental_model(model_id) for full observations",
|
||||
"3. DECOMPOSE the question into component searches (see Query Strategy above)",
|
||||
" - Identify entities and concepts in the question",
|
||||
" - Search for each separately with targeted queries",
|
||||
"4. Run multiple recall() calls - don't just echo the user's question",
|
||||
"5. Use expand() if you need more context on specific memories",
|
||||
"6. BEFORE answering: Check if any person/project/concept from the memories deserves a mental model - use learn() if so",
|
||||
"7. When ready, call done() with your answer and supporting memory_ids",
|
||||
"",
|
||||
"## When to Use learn() - IMPORTANT",
|
||||
"ACTIVELY look for opportunities to use learn() when you discover:",
|
||||
"- A person mentioned in 2+ memories who has no mental model yet",
|
||||
"- A project or concept the user asks about that has no mental model",
|
||||
"- A pattern or topic worth tracking for future questions",
|
||||
"",
|
||||
"DO NOT wait to be asked - proactively create models when you see the need.",
|
||||
"Example: learn(name='Project Alpha', description='Track goals, status, and key decisions for Project Alpha')",
|
||||
"",
|
||||
"## Output Format: Plain Text Answer",
|
||||
"Call done() with a plain text 'answer' field.",
|
||||
"- Do NOT use markdown formatting",
|
||||
"- NEVER include memory IDs, UUIDs, or 'Memory references' in the answer text",
|
||||
"- Put memory IDs ONLY in the memory_ids array parameter, not in the answer",
|
||||
]
|
||||
)
|
||||
# Mode-specific workflow and output format
|
||||
if output_mode == "observations":
|
||||
# Observations mode: for mental model generation - no mental model lookup tools
|
||||
parts.extend(
|
||||
[
|
||||
"1. DECOMPOSE the topic into component searches (see Query Strategy above)",
|
||||
" - Don't search for the topic name itself - search for related concepts",
|
||||
" - Example for 'Coffee preferences': search 'coffee', 'drinks', 'morning routine', 'caffeine'",
|
||||
"2. Run multiple recall() calls with varied, targeted queries",
|
||||
"3. IMPORTANT: Use expand(memory_ids, 'chunk') to verify memories before using them",
|
||||
" - Always verify the source chunk to confirm the memory is actually relevant",
|
||||
" - Don't assume a memory is relevant based on the summary alone",
|
||||
" - Only include memories you've verified via expand()",
|
||||
"4. When ready, call done() with MULTIPLE structured observations",
|
||||
"",
|
||||
"## Output Format: MULTIPLE Structured Observations",
|
||||
"",
|
||||
"CRITICAL: You MUST create MULTIPLE separate observations in the array - one for each theme.",
|
||||
"Do NOT put all content in a single observation!",
|
||||
"",
|
||||
"- Create 3-8 separate observations, each as its OWN item in the observations array",
|
||||
"- Each observation covers ONE specific theme (preferences, history, relationships, etc.)",
|
||||
"- Each observation has: title (short header), text (content), memory_ids (full UUIDs)",
|
||||
"",
|
||||
"Text format for each observation:",
|
||||
"- Main insight or finding (no markdown headers)",
|
||||
"- End with 'Key evidence:' section containing DIRECT QUOTES from memories in *italics*",
|
||||
"- Quote the actual memory text, don't summarize - use *italics* for citations",
|
||||
"",
|
||||
"Example done() call with MULTIPLE observations:",
|
||||
"```json",
|
||||
"{",
|
||||
' "observations": [',
|
||||
" {",
|
||||
' "title": "Work Preferences",',
|
||||
' "text": "Prefers async communication and flexible schedules.\\n\\nKey evidence:\\n- *I prefer Slack over calls for most communication*\\n- *Flexible hours help me do my best work*",',
|
||||
' "memory_ids": ["abc123-full-uuid", "def456-full-uuid"]',
|
||||
" },",
|
||||
" {",
|
||||
' "title": "Technical Background",',
|
||||
' "text": "Has extensive ML experience spanning a decade.\\n\\nKey evidence:\\n- *I have 10 years of experience in machine learning*\\n- *Led the ML team at my previous company*",',
|
||||
' "memory_ids": ["ghi789-full-uuid"]',
|
||||
" }",
|
||||
" ]",
|
||||
"}",
|
||||
"```",
|
||||
]
|
||||
)
|
||||
else:
|
||||
# Answer mode: include mental model lookup in workflow
|
||||
parts.extend(
|
||||
[
|
||||
"1. Review the pre-fetched mental models for relevant synthesized knowledge",
|
||||
"2. If relevant, call get_mental_model(model_id) for full observations",
|
||||
"3. DECOMPOSE the question into component searches (see Query Strategy above)",
|
||||
" - Identify entities and concepts in the question",
|
||||
" - Search for each separately with targeted queries",
|
||||
"4. Run multiple recall() calls - don't just echo the user's question",
|
||||
"5. Use expand() if you need more context on specific memories",
|
||||
"6. If you discover an important recurring topic worth tracking, use learn() to create a mental model",
|
||||
"7. When ready, call done() with your answer and supporting memory_ids",
|
||||
"",
|
||||
"## When to Use learn()",
|
||||
"Use learn() to create a new mental model when you discover:",
|
||||
"- A person, project, or concept that appears frequently in memories",
|
||||
"- An important topic the user seems to care about but has no mental model for",
|
||||
"- A pattern or relationship worth synthesizing for future reference",
|
||||
"Example: learn(name='Project Alpha', description='Track goals, status, and key decisions for Project Alpha')",
|
||||
"",
|
||||
"## Output Format: Plain Text Answer",
|
||||
"Call done() with a plain text 'answer' field.",
|
||||
"- Do NOT use markdown formatting",
|
||||
"- NEVER include memory IDs, UUIDs, or 'Memory references' in the answer text",
|
||||
"- Put memory IDs ONLY in the memory_ids array parameter, not in the answer",
|
||||
]
|
||||
)
|
||||
|
||||
parts.append("")
|
||||
parts.append(f"## Memory Bank: {name}")
|
||||
@@ -227,10 +164,6 @@ def build_system_prompt_for_tools(
|
||||
if context:
|
||||
parts.append(f"\n## Additional Context\n{context}")
|
||||
|
||||
# Add directive reminder at the END for recency effect
|
||||
if directives:
|
||||
parts.append(build_directives_reminder(directives))
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
@@ -377,386 +310,3 @@ Your approach:
|
||||
|
||||
Only say "I don't have information" if the retrieved data is truly unrelated to the question.
|
||||
Do NOT fabricate information that has no basis in the retrieved data."""
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 4-Phase Mental Model Reflect Prompts
|
||||
# =============================================================================
|
||||
|
||||
SEED_PHASE_SYSTEM_PROMPT = """You are analyzing memories to discover NEW patterns and generate candidate observations.
|
||||
|
||||
Your task is to identify potential observations (beliefs, preferences, patterns, behaviors) that could be part of a mental model about this person/topic.
|
||||
|
||||
## Important: Avoid Redundancy
|
||||
If existing observations are provided, DO NOT generate candidates that are essentially the same.
|
||||
Focus on discovering NEW patterns not already covered by existing observations.
|
||||
|
||||
## Rules
|
||||
- Generate 5-15 candidate observations for NEW patterns only
|
||||
- Each candidate should be specific and testable (can be supported or contradicted by evidence)
|
||||
- Note which memory IDs inspired each candidate (these are seeds, not final evidence)
|
||||
- Focus on patterns that appear MULTIPLE TIMES across many memories - the more the better
|
||||
- The best candidates are ones you can find 10, 20, or even 50+ supporting memories for
|
||||
- Skip patterns that are already covered by existing observations
|
||||
|
||||
## Output Format
|
||||
Return a JSON array of candidate observations:
|
||||
```json
|
||||
{
|
||||
"candidates": [
|
||||
{
|
||||
"content": "The specific observation/belief/pattern - be detailed and specific",
|
||||
"seed_memory_ids": ["memory_id_1", "memory_id_2", "memory_id_3"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Focus on patterns that appear multiple times or have strong signals. Don't generate obvious or trivial observations.
|
||||
Prefer candidates with MORE seed memories - they're more likely to be real patterns.
|
||||
Return an empty candidates array if no genuinely new patterns are found."""
|
||||
|
||||
|
||||
def build_seed_phase_prompt(
|
||||
memories: list[dict],
|
||||
topic: str | None = None,
|
||||
existing_observations: list[dict] | None = None,
|
||||
) -> str:
|
||||
"""Build the user prompt for the seed phase.
|
||||
|
||||
Args:
|
||||
memories: List of memories to analyze
|
||||
topic: Optional topic focus for the mental model
|
||||
existing_observations: Optional list of existing observations to avoid rediscovering
|
||||
"""
|
||||
parts = []
|
||||
|
||||
if topic:
|
||||
parts.append(f"## Topic Focus\n{topic}\n")
|
||||
|
||||
# Include existing observations so we don't rediscover them
|
||||
if existing_observations:
|
||||
parts.append("## Existing Observations (DO NOT regenerate these)")
|
||||
parts.append("These patterns are already tracked. Focus on discovering NEW patterns:\n")
|
||||
for i, obs in enumerate(existing_observations, 1):
|
||||
title = obs.get("title", "")
|
||||
content = obs.get("content", "")
|
||||
parts.append(f"{i}. **{title}**: {content}\n")
|
||||
parts.append("")
|
||||
|
||||
parts.append("## Memories to Analyze")
|
||||
parts.append("Review these memories and identify patterns, preferences, beliefs, and behaviors:\n")
|
||||
|
||||
for mem in memories:
|
||||
mem_id = mem.get("id", "unknown")
|
||||
content = mem.get("content", mem.get("text", ""))
|
||||
timestamp = mem.get("timestamp", mem.get("created_at", ""))
|
||||
parts.append(f"[{mem_id}] ({timestamp}): {content}\n")
|
||||
|
||||
parts.append("\n## Instructions")
|
||||
if existing_observations:
|
||||
parts.append("Generate candidate observations for NEW patterns not already covered above.")
|
||||
parts.append("If all patterns are already covered by existing observations, return an empty candidates array.")
|
||||
else:
|
||||
parts.append("Generate candidate observations based on patterns you see in these memories.")
|
||||
parts.append("Look for: recurring themes, stated preferences, behavioral patterns, beliefs, values, goals.")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
VALIDATE_PHASE_SYSTEM_PROMPT = """You are validating candidate observations against evidence.
|
||||
|
||||
For each candidate, you have:
|
||||
- Supporting memories (evidence FOR the observation)
|
||||
- Contradicting memories (evidence AGAINST the observation)
|
||||
|
||||
## Your Task
|
||||
1. Evaluate each candidate based on the evidence
|
||||
2. For valid candidates, extract EXACT QUOTES from supporting memories
|
||||
3. Discard candidates with insufficient or contradicting evidence
|
||||
4. Merge similar candidates into single, refined observations
|
||||
|
||||
## Rules for Quotes
|
||||
- Quotes must be EXACT text from the memory, not paraphrased
|
||||
- Each quote should directly support the observation
|
||||
- The MORE evidence quotes, the BETTER - don't limit yourself, include ALL relevant quotes (10, 20, 50+)
|
||||
- Observations with only 1-2 quotes are weak and should be discarded unless the evidence is exceptionally strong
|
||||
- Stronger observations have more supporting evidence - aim for comprehensive coverage
|
||||
|
||||
## Output Format
|
||||
Return validated observations with evidence:
|
||||
```json
|
||||
{
|
||||
"observations": [
|
||||
{
|
||||
"title": "Short descriptive title (3-8 words) - like a headline",
|
||||
"content": "The full observation content - detailed explanation of the pattern/belief",
|
||||
"evidence": [
|
||||
{
|
||||
"memory_id": "exact_memory_id",
|
||||
"quote": "Exact quote from the memory text",
|
||||
"relevance": "Brief explanation of how this supports the observation",
|
||||
"timestamp": "2024-01-15T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"discarded": [
|
||||
{
|
||||
"content": "The discarded candidate",
|
||||
"reason": "Why it was discarded (insufficient evidence, contradicted, etc.)"
|
||||
}
|
||||
],
|
||||
"merged": [
|
||||
{
|
||||
"from": ["candidate 1 content", "candidate 2 content"],
|
||||
"into": "The merged observation content"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Title Guidelines
|
||||
- Title should be a SHORT label (like "Prefers morning meetings" or "Coffee enthusiast")
|
||||
- NOT a truncated version of the content
|
||||
- Think of it as a category/tag for the observation
|
||||
|
||||
Be rigorous: only keep observations with clear, verifiable evidence from multiple memories."""
|
||||
|
||||
|
||||
def build_validate_phase_prompt(candidates_with_evidence: list[dict]) -> str:
|
||||
"""Build the user prompt for the validate phase."""
|
||||
parts = ["## Candidates to Validate\n"]
|
||||
|
||||
for i, item in enumerate(candidates_with_evidence, 1):
|
||||
candidate = item.get("candidate", {})
|
||||
supporting = item.get("supporting_memories", [])
|
||||
contradicting = item.get("contradicting_memories", [])
|
||||
|
||||
parts.append(f"### Candidate {i}: {candidate.get('content', '')}")
|
||||
|
||||
if supporting:
|
||||
parts.append("\n**Supporting Evidence:**")
|
||||
for mem in supporting:
|
||||
mem_id = mem.get("id", "unknown")
|
||||
content = mem.get("content", mem.get("text", ""))
|
||||
timestamp = mem.get("timestamp", mem.get("created_at", ""))
|
||||
parts.append(f"- [{mem_id}] ({timestamp}): {content}")
|
||||
|
||||
if contradicting:
|
||||
parts.append("\n**Contradicting Evidence:**")
|
||||
for mem in contradicting:
|
||||
mem_id = mem.get("id", "unknown")
|
||||
content = mem.get("content", mem.get("text", ""))
|
||||
timestamp = mem.get("timestamp", mem.get("created_at", ""))
|
||||
parts.append(f"- [{mem_id}] ({timestamp}): {content}")
|
||||
|
||||
if not supporting and not contradicting:
|
||||
parts.append("\n*No additional evidence found*")
|
||||
|
||||
parts.append("")
|
||||
|
||||
parts.append("## Instructions")
|
||||
parts.append("1. Evaluate each candidate based on its evidence")
|
||||
parts.append("2. Keep candidates with strong supporting evidence")
|
||||
parts.append("3. Discard candidates with no evidence or strong contradictions")
|
||||
parts.append("4. Merge similar candidates")
|
||||
parts.append("5. Extract EXACT quotes (copy-paste from memory text) for evidence")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
COMPARE_PHASE_SYSTEM_PROMPT = """You are merging new observations with an existing mental model.
|
||||
|
||||
You have:
|
||||
- EXISTING observations (from the current mental model)
|
||||
- NEW observations (from this reflect cycle)
|
||||
|
||||
## Your Task
|
||||
Produce the final, complete mental model by:
|
||||
1. Keeping existing observations that are still valid
|
||||
2. Updating existing observations with new evidence (ADD new evidence to existing)
|
||||
3. Adding new observations that don't overlap with existing
|
||||
4. Removing existing observations that are contradicted by new evidence
|
||||
5. Merging overlapping observations
|
||||
|
||||
## Rules
|
||||
- The final model should have no contradictions
|
||||
- Each observation must have evidence with exact quotes
|
||||
- COMBINE evidence from both existing and new observations
|
||||
- If an existing observation has new supporting evidence, ADD ALL the new evidence to it
|
||||
- Include ALL relevant evidence - the more quotes the better (10, 20, 50+ is great)
|
||||
- Observations with more evidence are more reliable - don't limit the number of quotes
|
||||
|
||||
## Output Format
|
||||
Return the complete, final mental model:
|
||||
```json
|
||||
{
|
||||
"observations": [
|
||||
{
|
||||
"title": "Short descriptive title (3-8 words)",
|
||||
"content": "Full observation content - detailed explanation",
|
||||
"evidence": [
|
||||
{
|
||||
"memory_id": "id",
|
||||
"quote": "exact quote",
|
||||
"relevance": "explanation",
|
||||
"timestamp": "ISO timestamp"
|
||||
}
|
||||
],
|
||||
"created_at": "ISO timestamp of when observation was first created"
|
||||
}
|
||||
],
|
||||
"changes": {
|
||||
"kept": ["Observation that was kept unchanged"],
|
||||
"updated": [{"from": "old content", "to": "new content", "reason": "why"}],
|
||||
"added": ["New observation that was added"],
|
||||
"removed": [{"content": "removed observation", "reason": "why removed"}],
|
||||
"merged": [{"from": ["obs1", "obs2"], "into": "merged observation"}]
|
||||
}
|
||||
}
|
||||
```"""
|
||||
|
||||
|
||||
def build_compare_phase_prompt(
|
||||
existing_observations: list[dict],
|
||||
new_observations: list[dict],
|
||||
) -> str:
|
||||
"""Build the user prompt for the compare phase."""
|
||||
parts = []
|
||||
|
||||
parts.append("## Existing Mental Model Observations")
|
||||
if existing_observations:
|
||||
for i, obs in enumerate(existing_observations, 1):
|
||||
title = obs.get("title", "")
|
||||
content = obs.get("content", obs.get("text", ""))
|
||||
evidence = obs.get("evidence", [])
|
||||
parts.append(f"\n### Existing {i}: {title}")
|
||||
parts.append(f"Content: {content}")
|
||||
if evidence:
|
||||
parts.append(f"Evidence ({len(evidence)} items):")
|
||||
for ev in evidence[:5]: # Show max 5 evidence items
|
||||
parts.append(f' - [{ev.get("memory_id", "?")}]: "{ev.get("quote", "")}"')
|
||||
if len(evidence) > 5:
|
||||
parts.append(f" ... and {len(evidence) - 5} more")
|
||||
else:
|
||||
parts.append("*No existing observations*")
|
||||
|
||||
parts.append("\n## New Observations from This Reflect")
|
||||
if new_observations:
|
||||
for i, obs in enumerate(new_observations, 1):
|
||||
title = obs.get("title", "")
|
||||
content = obs.get("content", "")
|
||||
evidence = obs.get("evidence", [])
|
||||
parts.append(f"\n### New {i}: {title}")
|
||||
parts.append(f"Content: {content}")
|
||||
if evidence:
|
||||
parts.append(f"Evidence ({len(evidence)} items):")
|
||||
for ev in evidence:
|
||||
parts.append(f' - [{ev.get("memory_id", "?")}]: "{ev.get("quote", "")}"')
|
||||
else:
|
||||
parts.append("*No new observations*")
|
||||
|
||||
parts.append("\n## Instructions")
|
||||
parts.append("Merge these into a coherent, non-contradictory mental model.")
|
||||
parts.append("Preserve all valid evidence. Remove stale or contradicted observations.")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# UPDATE EXISTING Phase Prompts (for diff-based refresh)
|
||||
# =============================================================================
|
||||
|
||||
UPDATE_EXISTING_SYSTEM_PROMPT = """You are updating existing observations with newly found evidence.
|
||||
|
||||
For each existing observation, you have been given:
|
||||
- The original observation (title, content, existing evidence)
|
||||
- Newly found supporting memories
|
||||
- Newly found contradicting memories
|
||||
|
||||
## Your Task
|
||||
1. Extract EXACT QUOTES from new supporting memories to add to the observation
|
||||
2. Flag observations with strong contradicting evidence for potential removal
|
||||
3. Keep existing evidence intact - only ADD new evidence
|
||||
|
||||
## Rules for Quotes
|
||||
- Quotes must be EXACT text from the memory, not paraphrased
|
||||
- Each quote should directly support the observation
|
||||
- Include ALL relevant quotes from the new memories
|
||||
|
||||
## Output Format
|
||||
Return updated observations with new evidence:
|
||||
```json
|
||||
{
|
||||
"updated_observations": [
|
||||
{
|
||||
"title": "Original title",
|
||||
"content": "Original content",
|
||||
"existing_evidence_count": 5,
|
||||
"new_evidence": [
|
||||
{
|
||||
"memory_id": "exact_memory_id",
|
||||
"quote": "Exact quote from the memory text",
|
||||
"relevance": "Brief explanation of how this supports the observation",
|
||||
"timestamp": "2024-01-15T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"has_contradiction": false,
|
||||
"contradiction_note": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If an observation has strong contradicting evidence, set has_contradiction=true and explain in contradiction_note."""
|
||||
|
||||
|
||||
def build_update_existing_prompt(observations_with_evidence: list[dict]) -> str:
|
||||
"""Build the user prompt for the update existing phase.
|
||||
|
||||
Args:
|
||||
observations_with_evidence: List of existing observations with new evidence found
|
||||
"""
|
||||
parts = ["## Existing Observations to Update\n"]
|
||||
|
||||
for i, item in enumerate(observations_with_evidence, 1):
|
||||
obs = item.get("observation", {})
|
||||
supporting = item.get("supporting_memories", [])
|
||||
contradicting = item.get("contradicting_memories", [])
|
||||
|
||||
title = obs.get("title", "")
|
||||
content = obs.get("content", "")
|
||||
existing_evidence = obs.get("evidence", [])
|
||||
|
||||
parts.append(f"### Observation {i}: {title}")
|
||||
parts.append(f"Content: {content}")
|
||||
parts.append(f"Existing evidence count: {len(existing_evidence)}")
|
||||
|
||||
if supporting:
|
||||
parts.append("\n**New Supporting Memories:**")
|
||||
for mem in supporting:
|
||||
mem_id = mem.get("id", "unknown")
|
||||
mem_content = mem.get("content", mem.get("text", ""))
|
||||
timestamp = mem.get("timestamp", mem.get("created_at", ""))
|
||||
parts.append(f"- [{mem_id}] ({timestamp}): {mem_content}")
|
||||
|
||||
if contradicting:
|
||||
parts.append("\n**New Contradicting Memories:**")
|
||||
for mem in contradicting:
|
||||
mem_id = mem.get("id", "unknown")
|
||||
mem_content = mem.get("content", mem.get("text", ""))
|
||||
timestamp = mem.get("timestamp", mem.get("created_at", ""))
|
||||
parts.append(f"- [{mem_id}] ({timestamp}): {mem_content}")
|
||||
|
||||
if not supporting and not contradicting:
|
||||
parts.append("\n*No new evidence found*")
|
||||
|
||||
parts.append("")
|
||||
|
||||
parts.append("## Instructions")
|
||||
parts.append("1. Extract EXACT quotes from new supporting memories")
|
||||
parts.append("2. Flag observations with strong contradictions")
|
||||
parts.append("3. Return the updated observations with new evidence added")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
@@ -5,11 +5,9 @@ Tool implementations for the reflect agent.
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from .models import MentalModelInput
|
||||
from .observations import Observation, ObservationEvidence, Trend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from asyncpg import Connection
|
||||
@@ -28,37 +26,6 @@ def generate_model_id(name: str) -> str:
|
||||
return normalized[:50]
|
||||
|
||||
|
||||
def _parse_observations(observations_raw: list) -> list[Observation]:
|
||||
"""Parse raw observation dicts into typed Observation models."""
|
||||
observations: list[Observation] = []
|
||||
for obs in observations_raw:
|
||||
if not isinstance(obs, dict):
|
||||
continue
|
||||
|
||||
try:
|
||||
parsed = Observation(
|
||||
title=obs.get("title", ""),
|
||||
content=obs.get("content", ""),
|
||||
evidence=[
|
||||
ObservationEvidence(
|
||||
memory_id=ev.get("memory_id", ""),
|
||||
quote=ev.get("quote", ""),
|
||||
relevance=ev.get("relevance", ""),
|
||||
timestamp=ev.get("timestamp"),
|
||||
)
|
||||
for ev in obs.get("evidence", [])
|
||||
if isinstance(ev, dict)
|
||||
],
|
||||
created_at=obs.get("created_at"),
|
||||
)
|
||||
observations.append(parsed)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to parse observation: {e}")
|
||||
continue
|
||||
|
||||
return observations
|
||||
|
||||
|
||||
async def tool_lookup(
|
||||
conn: "Connection",
|
||||
bank_id: str,
|
||||
@@ -99,8 +66,18 @@ async def tool_lookup(
|
||||
obs_data = json.loads(obs_data)
|
||||
observations_raw = obs_data.get("observations", []) if isinstance(obs_data, dict) else obs_data
|
||||
|
||||
# Parse observations into typed models
|
||||
observations = _parse_observations(observations_raw)
|
||||
# Normalize observation format: map memory_ids/fact_ids to based_on
|
||||
observations = []
|
||||
for obs in observations_raw:
|
||||
if isinstance(obs, dict):
|
||||
based_on = obs.get("memory_ids") or obs.get("fact_ids") or []
|
||||
observations.append(
|
||||
{
|
||||
"title": obs.get("title", ""),
|
||||
"text": obs.get("text", ""),
|
||||
"based_on": based_on,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"found": True,
|
||||
@@ -109,7 +86,7 @@ async def tool_lookup(
|
||||
"subtype": row["subtype"],
|
||||
"name": row["name"],
|
||||
"description": row["description"],
|
||||
"observations": observations,
|
||||
"observations": observations, # [{title, text, based_on}, ...]
|
||||
"entity_id": str(row["entity_id"]) if row["entity_id"] else None,
|
||||
"last_updated": row["last_updated"].isoformat() if row["last_updated"] else None,
|
||||
},
|
||||
@@ -118,8 +95,6 @@ async def tool_lookup(
|
||||
else:
|
||||
# List mental models (compact: id, name, description only)
|
||||
# Full observations are retrieved via get_mental_model(model_id)
|
||||
# NOTE: Directives (subtype='directive') are excluded from listing -
|
||||
# they are injected into the system prompt, not discoverable via tools
|
||||
# Filter by tags if provided
|
||||
if tags:
|
||||
if tags_match == "all":
|
||||
@@ -128,7 +103,7 @@ async def tool_lookup(
|
||||
"""
|
||||
SELECT id, subtype, name, description
|
||||
FROM mental_models
|
||||
WHERE bank_id = $1 AND tags @> $2::varchar[] AND subtype != 'directive'
|
||||
WHERE bank_id = $1 AND tags @> $2::varchar[]
|
||||
ORDER BY last_updated DESC NULLS LAST, created_at DESC
|
||||
""",
|
||||
bank_id,
|
||||
@@ -140,7 +115,7 @@ async def tool_lookup(
|
||||
"""
|
||||
SELECT id, subtype, name, description
|
||||
FROM mental_models
|
||||
WHERE bank_id = $1 AND tags && $2::varchar[] AND subtype != 'directive'
|
||||
WHERE bank_id = $1 AND tags && $2::varchar[]
|
||||
ORDER BY last_updated DESC NULLS LAST, created_at DESC
|
||||
""",
|
||||
bank_id,
|
||||
@@ -151,7 +126,7 @@ async def tool_lookup(
|
||||
"""
|
||||
SELECT id, subtype, name, description
|
||||
FROM mental_models
|
||||
WHERE bank_id = $1 AND subtype != 'directive'
|
||||
WHERE bank_id = $1
|
||||
ORDER BY last_updated DESC NULLS LAST, created_at DESC
|
||||
""",
|
||||
bank_id,
|
||||
|
||||
@@ -4,6 +4,8 @@ Tool schema definitions for the reflect agent.
|
||||
These are OpenAI-format tool definitions used with native tool calling.
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
# Tool definitions in OpenAI format
|
||||
TOOL_LIST_MENTAL_MODELS = {
|
||||
"type": "function",
|
||||
@@ -132,76 +134,68 @@ TOOL_DONE_ANSWER = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_done_tool_with_directives(directive_rules: list[str]) -> dict:
|
||||
"""
|
||||
Build the done tool schema with directive compliance field.
|
||||
|
||||
When directives are present, adds a required field that forces the agent
|
||||
to confirm compliance with each directive before submitting.
|
||||
|
||||
Args:
|
||||
directive_rules: List of directive rule strings
|
||||
"""
|
||||
from typing import Any, cast
|
||||
|
||||
# Build rules list for description
|
||||
rules_list = "\n".join(f" {i + 1}. {rule}" for i, rule in enumerate(directive_rules))
|
||||
|
||||
# Build the tool with directive compliance field
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "done",
|
||||
"description": (
|
||||
"Signal completion with your final answer. IMPORTANT: You must confirm directive compliance before submitting. "
|
||||
"Your answer will be REJECTED if it violates any directive."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"answer": {
|
||||
"type": "string",
|
||||
"description": "Your response as plain text. Do NOT use markdown formatting. NEVER include memory IDs, UUIDs, or 'Memory references' in this text - put IDs only in memory_ids array.",
|
||||
},
|
||||
"memory_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Array of memory IDs that support your answer (put IDs here, NOT in answer text)",
|
||||
},
|
||||
"model_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Array of mental model IDs that support your answer",
|
||||
},
|
||||
"directive_compliance": {
|
||||
"type": "string",
|
||||
"description": f"REQUIRED: Confirm your answer complies with ALL directives. List each directive and how your answer follows it:\n{rules_list}\n\nFormat: 'Directive 1: [how answer complies]. Directive 2: [how answer complies]...'",
|
||||
TOOL_DONE_OBSERVATIONS = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "done",
|
||||
"description": "Signal completion with MULTIPLE structured observations. Each observation must be a SEPARATE item in the array covering ONE theme. Do NOT combine all content into a single observation.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"observations": {
|
||||
"type": "array",
|
||||
"minItems": 3,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Short header for this observation's theme (e.g., 'Work Style', 'Technical Skills')",
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Observation content about ONE theme. End with 'Key evidence:' containing text citations (summaries of what memories say), NOT memory IDs.",
|
||||
},
|
||||
"memory_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Full UUIDs of memories supporting this observation (put IDs here, not in text)",
|
||||
},
|
||||
},
|
||||
"required": ["title", "text", "memory_ids"],
|
||||
},
|
||||
"description": "Array of 3-8 observations, each covering a DIFFERENT aspect/theme. Do NOT put everything in one observation.",
|
||||
},
|
||||
"required": ["answer", "directive_compliance"],
|
||||
},
|
||||
"required": ["observations"],
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_reflect_tools(enable_learn: bool = True, directive_rules: list[str] | None = None) -> list[dict]:
|
||||
def get_reflect_tools(
|
||||
enable_learn: bool = True, output_mode: Literal["answer", "observations"] = "answer"
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Get the list of tools for the reflect agent.
|
||||
|
||||
Args:
|
||||
enable_learn: Whether to include the learn tool
|
||||
directive_rules: Optional list of directive rule strings. If provided,
|
||||
the done() tool will require directive compliance confirmation.
|
||||
output_mode: "answer" or "observations" - determines done tool format
|
||||
In observations mode, mental model tools are excluded to avoid
|
||||
using potentially outdated models during regeneration.
|
||||
|
||||
Returns:
|
||||
List of tool definitions in OpenAI format
|
||||
"""
|
||||
tools = []
|
||||
|
||||
# Include mental model tools for lookup
|
||||
tools.append(TOOL_LIST_MENTAL_MODELS)
|
||||
tools.append(TOOL_GET_MENTAL_MODEL)
|
||||
# In answer mode, include mental model tools for lookup
|
||||
# In observations mode (mental model generation), exclude them to avoid circular references
|
||||
if output_mode == "answer":
|
||||
tools.append(TOOL_LIST_MENTAL_MODELS)
|
||||
tools.append(TOOL_GET_MENTAL_MODEL)
|
||||
|
||||
tools.append(TOOL_RECALL)
|
||||
|
||||
if enable_learn:
|
||||
@@ -209,9 +203,9 @@ def get_reflect_tools(enable_learn: bool = True, directive_rules: list[str] | No
|
||||
|
||||
tools.append(TOOL_EXPAND)
|
||||
|
||||
# Use directive-aware done tool if directives are present
|
||||
if directive_rules:
|
||||
tools.append(_build_done_tool_with_directives(directive_rules))
|
||||
# Add appropriate done tool based on output mode
|
||||
if output_mode == "observations":
|
||||
tools.append(TOOL_DONE_OBSERVATIONS)
|
||||
else:
|
||||
tools.append(TOOL_DONE_ANSWER)
|
||||
|
||||
|
||||
@@ -58,14 +58,6 @@ class MentalModelRef(BaseModel):
|
||||
summary: str | None = Field(default=None, description="Full summary (when looked up in detail)")
|
||||
|
||||
|
||||
class DirectiveRef(BaseModel):
|
||||
"""Reference to a directive that was applied during reflect."""
|
||||
|
||||
id: str = Field(description="Directive mental model ID")
|
||||
name: str = Field(description="Directive name")
|
||||
rules: list[str] = Field(default_factory=list, description="Directive rules/observations that were applied")
|
||||
|
||||
|
||||
class TokenUsage(BaseModel):
|
||||
"""
|
||||
Token usage metrics for LLM calls.
|
||||
@@ -260,11 +252,7 @@ class ReflectResult(BaseModel):
|
||||
)
|
||||
mental_models: list[MentalModelRef] = Field(
|
||||
default_factory=list,
|
||||
description="Mental models accessed during reflection, including directives (subtype='directive').",
|
||||
)
|
||||
directives_applied: list[DirectiveRef] = Field(
|
||||
default_factory=list,
|
||||
description="Directive mental models that were applied during this reflection.",
|
||||
description="Mental models accessed during reflection. Only present when include.facts is enabled.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@ from hindsight_api.extensions.operation_validator import (
|
||||
RecallResult,
|
||||
ReflectContext,
|
||||
ReflectResultContext,
|
||||
RefreshMentalModelContext,
|
||||
RefreshMentalModelResult,
|
||||
RetainContext,
|
||||
RetainResult,
|
||||
ValidationResult,
|
||||
@@ -56,8 +54,6 @@ __all__ = [
|
||||
"RecallResult",
|
||||
"ReflectContext",
|
||||
"ReflectResultContext",
|
||||
"RefreshMentalModelContext",
|
||||
"RefreshMentalModelResult",
|
||||
"RetainContext",
|
||||
"RetainResult",
|
||||
"ValidationResult",
|
||||
|
||||
@@ -97,18 +97,6 @@ class ReflectContext:
|
||||
context: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefreshMentalModelContext:
|
||||
"""Context for a refresh mental model operation validation (pre-operation).
|
||||
|
||||
Contains ALL user-provided parameters for the refresh mental model operation.
|
||||
"""
|
||||
|
||||
bank_id: str
|
||||
model_id: str
|
||||
request_context: "RequestContext"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Post-operation Contexts (includes results)
|
||||
# =============================================================================
|
||||
@@ -176,27 +164,6 @@ class ReflectResultContext:
|
||||
error: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefreshMentalModelResult:
|
||||
"""Result context for post-refresh-mental-model hook.
|
||||
|
||||
Contains the operation parameters and the result including token usage.
|
||||
"""
|
||||
|
||||
bank_id: str
|
||||
model_id: str
|
||||
request_context: "RequestContext"
|
||||
# Result
|
||||
model_name: str | None = None
|
||||
observations_count: int = 0
|
||||
input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
total_tokens: int = 0
|
||||
duration_ms: int = 0
|
||||
success: bool = True
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class OperationValidatorExtension(Extension, ABC):
|
||||
"""
|
||||
Validates and hooks into retain/recall/reflect operations.
|
||||
@@ -298,25 +265,6 @@ class OperationValidatorExtension(Extension, ABC):
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def validate_refresh_mental_model(self, ctx: RefreshMentalModelContext) -> ValidationResult:
|
||||
"""
|
||||
Validate a refresh mental model operation before execution.
|
||||
|
||||
Called before the refresh mental model operation is processed.
|
||||
Return ValidationResult.reject() to prevent the operation from executing.
|
||||
|
||||
Args:
|
||||
ctx: Context containing all user-provided parameters:
|
||||
- bank_id: Bank identifier
|
||||
- model_id: Mental model ID to refresh
|
||||
- request_context: Request context with auth info
|
||||
|
||||
Returns:
|
||||
ValidationResult indicating whether the operation is allowed.
|
||||
"""
|
||||
...
|
||||
|
||||
# =========================================================================
|
||||
# Post-operation hooks (optional - override to implement)
|
||||
# =========================================================================
|
||||
@@ -377,28 +325,3 @@ class OperationValidatorExtension(Extension, ABC):
|
||||
- error: Error message (if failed)
|
||||
"""
|
||||
pass
|
||||
|
||||
async def on_refresh_mental_model_complete(self, result: RefreshMentalModelResult) -> None:
|
||||
"""
|
||||
Called after a refresh mental model operation completes (success or failure).
|
||||
|
||||
Override this method to implement post-operation logic such as:
|
||||
- Token usage tracking and billing
|
||||
- Audit logging
|
||||
- Metrics collection
|
||||
|
||||
Args:
|
||||
result: Result context containing:
|
||||
- bank_id: Bank identifier
|
||||
- model_id: Mental model ID
|
||||
- request_context: Request context with auth info
|
||||
- model_name: Name of the mental model (if success)
|
||||
- observations_count: Number of observations generated
|
||||
- input_tokens: Number of input tokens used
|
||||
- output_tokens: Number of output tokens used
|
||||
- total_tokens: Total tokens used (input + output)
|
||||
- duration_ms: Total operation duration in milliseconds
|
||||
- success: Whether the operation succeeded
|
||||
- error: Error message (if failed)
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -199,7 +199,6 @@ def main():
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
log_level=args.log_level,
|
||||
log_format=config.log_format,
|
||||
mcp_enabled=config.mcp_enabled,
|
||||
graph_retriever=config.graph_retriever,
|
||||
mpfp_top_k_neighbors=config.mpfp_top_k_neighbors,
|
||||
|
||||
@@ -25,7 +25,7 @@ dependencies = [
|
||||
"psycopg2-binary>=2.9.11",
|
||||
"tiktoken>=0.12.0",
|
||||
"httpx>=0.27.0",
|
||||
"fastmcp>=2.14.0", # CVE-2025-66416
|
||||
"fastmcp>=2.3.0",
|
||||
"pg0-embedded>=0.11.0",
|
||||
"python-dateutil>=2.8.0",
|
||||
"opentelemetry-api>=1.20.0",
|
||||
@@ -39,17 +39,10 @@ dependencies = [
|
||||
"cohere>=5.0.0",
|
||||
"flashrank>=0.2.0",
|
||||
# Local ML models for embeddings/reranking - can be excluded in Docker with INCLUDE_LOCAL_MODELS=false
|
||||
"sentence-transformers>=3.3.0",
|
||||
"transformers>=4.53.0", # Security fixes for ReDoS vulnerabilities
|
||||
"torch>=2.6.0", # CVE fix for remote code execution
|
||||
"sentence-transformers>=3.0.0,<3.3.0",
|
||||
"transformers>=4.30.0,<4.46.0",
|
||||
"torch>=2.0.0",
|
||||
"uvloop>=0.22.1",
|
||||
# Transitive dependency security fixes
|
||||
"pyasn1>=0.6.2", # DoS vulnerability fix
|
||||
"urllib3>=2.6.3", # Decompression-bomb safeguards bypass fix
|
||||
"langchain-core>=1.2.5", # Serialization injection vulnerability fix
|
||||
"filelock>=3.20.1", # TOCTOU race condition fix
|
||||
"authlib>=1.6.6", # Account takeover vulnerability fix
|
||||
"aiohttp>=3.13.3", # Multiple DoS vulnerabilities
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -58,7 +51,7 @@ test = [
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"pytest-timeout>=2.4.0",
|
||||
"pytest-xdist>=3.0.0",
|
||||
"filelock>=3.20.1", # TOCTOU race condition fix
|
||||
"filelock>=3.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -104,7 +97,7 @@ dev = [
|
||||
"pytest-timeout>=2.4.0",
|
||||
"pytest-xdist>=3.8.0",
|
||||
"python-dotenv>=1.2.1",
|
||||
"filelock>=3.20.1", # TOCTOU race condition fix
|
||||
"filelock>=3.0.0",
|
||||
"ruff>=0.8.0",
|
||||
"ty>=0.0.1",
|
||||
]
|
||||
|
||||
@@ -17,8 +17,6 @@ from hindsight_api.extensions import (
|
||||
RecallResult,
|
||||
ReflectContext,
|
||||
ReflectResultContext,
|
||||
RefreshMentalModelContext,
|
||||
RefreshMentalModelResult,
|
||||
RequestContext,
|
||||
RetainContext,
|
||||
RetainResult,
|
||||
@@ -95,7 +93,6 @@ class RateLimitingValidator(OperationValidatorExtension):
|
||||
self.retain_counts: dict[str, int] = defaultdict(int)
|
||||
self.recall_counts: dict[str, int] = defaultdict(int)
|
||||
self.reflect_counts: dict[str, int] = defaultdict(int)
|
||||
self.refresh_mental_model_counts: dict[str, int] = defaultdict(int)
|
||||
|
||||
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
|
||||
self.retain_counts[ctx.bank_id] += 1
|
||||
@@ -121,16 +118,6 @@ class RateLimitingValidator(OperationValidatorExtension):
|
||||
)
|
||||
return ValidationResult.accept()
|
||||
|
||||
async def validate_refresh_mental_model(
|
||||
self, ctx: RefreshMentalModelContext
|
||||
) -> ValidationResult:
|
||||
self.refresh_mental_model_counts[ctx.bank_id] += 1
|
||||
if self.refresh_mental_model_counts[ctx.bank_id] > self.max_attempts:
|
||||
return ValidationResult.reject(
|
||||
f"Refresh mental model limit exceeded for bank {ctx.bank_id}"
|
||||
)
|
||||
return ValidationResult.accept()
|
||||
|
||||
|
||||
class TrackingValidator(OperationValidatorExtension):
|
||||
"""
|
||||
@@ -145,12 +132,10 @@ class TrackingValidator(OperationValidatorExtension):
|
||||
self.pre_retain_calls: list[RetainContext] = []
|
||||
self.pre_recall_calls: list[RecallContext] = []
|
||||
self.pre_reflect_calls: list[ReflectContext] = []
|
||||
self.pre_refresh_mental_model_calls: list[RefreshMentalModelContext] = []
|
||||
# Post-hook tracking
|
||||
self.post_retain_calls: list[RetainResult] = []
|
||||
self.post_recall_calls: list[RecallResult] = []
|
||||
self.post_reflect_calls: list[ReflectResultContext] = []
|
||||
self.post_refresh_mental_model_calls: list[RefreshMentalModelResult] = []
|
||||
|
||||
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
|
||||
self.pre_retain_calls.append(ctx)
|
||||
@@ -164,12 +149,6 @@ class TrackingValidator(OperationValidatorExtension):
|
||||
self.pre_reflect_calls.append(ctx)
|
||||
return ValidationResult.accept()
|
||||
|
||||
async def validate_refresh_mental_model(
|
||||
self, ctx: RefreshMentalModelContext
|
||||
) -> ValidationResult:
|
||||
self.pre_refresh_mental_model_calls.append(ctx)
|
||||
return ValidationResult.accept()
|
||||
|
||||
async def on_retain_complete(self, result: RetainResult) -> None:
|
||||
self.post_retain_calls.append(result)
|
||||
|
||||
@@ -179,11 +158,6 @@ class TrackingValidator(OperationValidatorExtension):
|
||||
async def on_reflect_complete(self, result: ReflectResultContext) -> None:
|
||||
self.post_reflect_calls.append(result)
|
||||
|
||||
async def on_refresh_mental_model_complete(
|
||||
self, result: RefreshMentalModelResult
|
||||
) -> None:
|
||||
self.post_refresh_mental_model_calls.append(result)
|
||||
|
||||
|
||||
class TestMemoryEngineValidation:
|
||||
"""Tests for validation integration with MemoryEngine.
|
||||
@@ -541,105 +515,6 @@ class TestOperationHooksParameters:
|
||||
assert len(validator.pre_recall_calls) == 1
|
||||
assert len(validator.post_recall_calls) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_mental_model_pre_hook_receives_all_parameters(
|
||||
self, memory_with_tracking_validator
|
||||
):
|
||||
"""Pre-refresh-mental-model hook receives all user-provided parameters."""
|
||||
import uuid
|
||||
|
||||
memory, validator = memory_with_tracking_validator
|
||||
bank_id = f"test-refresh-mm-params-{uuid.uuid4().hex[:8]}"
|
||||
ctx = RequestContext(api_key="test-key")
|
||||
|
||||
# Create bank first (get_bank_profile auto-creates if needed)
|
||||
await memory.get_bank_profile(bank_id, request_context=ctx)
|
||||
|
||||
# Create a pinned mental model
|
||||
model = await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Test Model",
|
||||
description="Test description",
|
||||
subtype="pinned",
|
||||
request_context=ctx,
|
||||
)
|
||||
|
||||
assert model is not None
|
||||
model_id = model["id"]
|
||||
|
||||
# Attempt to refresh (may not actually refresh if no data, but hook should be called)
|
||||
try:
|
||||
await memory.refresh_mental_model(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
request_context=ctx,
|
||||
)
|
||||
except Exception:
|
||||
pass # May fail if no data
|
||||
|
||||
# Check pre-hook was called
|
||||
assert len(validator.pre_refresh_mental_model_calls) == 1
|
||||
pre_ctx = validator.pre_refresh_mental_model_calls[0]
|
||||
assert pre_ctx.bank_id == bank_id
|
||||
assert pre_ctx.model_id == model_id
|
||||
assert pre_ctx.request_context == ctx
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_mental_model_post_hook_receives_token_usage(
|
||||
self, memory_with_tracking_validator
|
||||
):
|
||||
"""Post-refresh-mental-model hook receives token usage information."""
|
||||
import uuid
|
||||
|
||||
memory, validator = memory_with_tracking_validator
|
||||
bank_id = f"test-refresh-mm-tokens-{uuid.uuid4().hex[:8]}"
|
||||
ctx = RequestContext(api_key="test-key")
|
||||
|
||||
# Store some content first
|
||||
await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=[
|
||||
{"content": "Alice is a software engineer who works on machine learning."},
|
||||
{"content": "Alice enjoys hiking and outdoor activities on weekends."},
|
||||
{"content": "Alice has been working at the company for 5 years."},
|
||||
],
|
||||
request_context=ctx,
|
||||
)
|
||||
|
||||
# Create a pinned mental model
|
||||
model = await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Alice Profile",
|
||||
description="Profile of Alice including work and hobbies",
|
||||
subtype="pinned",
|
||||
request_context=ctx,
|
||||
)
|
||||
|
||||
if model:
|
||||
model_id = model["id"]
|
||||
|
||||
# Refresh the mental model
|
||||
result = await memory.refresh_mental_model(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
request_context=ctx,
|
||||
)
|
||||
|
||||
# Check post-hook was called with token usage
|
||||
if validator.post_refresh_mental_model_calls:
|
||||
post_result = validator.post_refresh_mental_model_calls[0]
|
||||
assert post_result.bank_id == bank_id
|
||||
assert post_result.model_id == model_id
|
||||
assert post_result.request_context == ctx
|
||||
assert post_result.success is True
|
||||
assert post_result.error is None
|
||||
|
||||
# Token usage should be populated (may be 0 if refresh was skipped)
|
||||
assert post_result.total_tokens >= 0
|
||||
assert post_result.input_tokens >= 0
|
||||
assert post_result.output_tokens >= 0
|
||||
assert post_result.duration_ms >= 0
|
||||
|
||||
|
||||
class TestTenantExtension:
|
||||
"""Tests for TenantExtension and ApiKeyTenantExtension."""
|
||||
|
||||
@@ -259,11 +259,23 @@ class TestReflectToolSchemas:
|
||||
assert "recall" in tool_names
|
||||
assert "done" in tool_names
|
||||
|
||||
def test_get_reflect_tools_observations_mode(self):
|
||||
"""Test getting reflect tools with observations output mode."""
|
||||
from hindsight_api.engine.reflect.tools_schema import get_reflect_tools
|
||||
|
||||
tools = get_reflect_tools(output_mode="observations")
|
||||
|
||||
done_tool = next(t for t in tools if t["function"]["name"] == "done")
|
||||
params = done_tool["function"]["parameters"]["properties"]
|
||||
|
||||
assert "observations" in params
|
||||
assert "answer" not in params
|
||||
|
||||
def test_get_reflect_tools_answer_mode(self):
|
||||
"""Test getting reflect tools with answer output mode."""
|
||||
from hindsight_api.engine.reflect.tools_schema import get_reflect_tools
|
||||
|
||||
tools = get_reflect_tools()
|
||||
tools = get_reflect_tools(output_mode="answer")
|
||||
|
||||
done_tool = next(t for t in tools if t["function"]["name"] == "done")
|
||||
params = done_tool["function"]["parameters"]["properties"]
|
||||
|
||||
@@ -363,7 +363,6 @@ from hindsight_api.extensions import (
|
||||
RetainContext,
|
||||
RecallContext,
|
||||
ReflectContext,
|
||||
RefreshMentalModelContext,
|
||||
)
|
||||
|
||||
|
||||
@@ -395,6 +394,3 @@ class MockOperationValidator(OperationValidatorExtension):
|
||||
|
||||
async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
|
||||
return ValidationResult.accept()
|
||||
|
||||
async def validate_refresh_mental_model(self, ctx: RefreshMentalModelContext) -> ValidationResult:
|
||||
return ValidationResult.accept()
|
||||
|
||||
@@ -793,642 +793,3 @@ class TestMentalModelTags:
|
||||
)
|
||||
assert "tags" in model
|
||||
assert isinstance(model["tags"], list)
|
||||
|
||||
|
||||
class TestDirectives:
|
||||
"""Test directive mental model functionality."""
|
||||
|
||||
async def test_create_directive(self, memory: MemoryEngine, request_context):
|
||||
"""Test creating a directive mental model with user-provided observations."""
|
||||
bank_id = f"test-directive-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Ensure bank exists
|
||||
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
|
||||
# Create a directive with observations
|
||||
model = await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Competitor Policy",
|
||||
description="Rules about mentioning competitors",
|
||||
subtype="directive",
|
||||
observations=[
|
||||
{"title": "Never mention", "content": "Never mention competitor product names directly"},
|
||||
{"title": "Redirect", "content": "If asked about competitors, redirect to our features"},
|
||||
],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert model["name"] == "Competitor Policy"
|
||||
assert model["description"] == "Rules about mentioning competitors"
|
||||
assert model["subtype"] == "directive"
|
||||
assert len(model["observations"]) == 2
|
||||
assert model["observations"][0].title == "Never mention"
|
||||
assert model["observations"][0].content == "Never mention competitor product names directly"
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
async def test_directive_included_in_list(self, memory: MemoryEngine, request_context):
|
||||
"""Test that directives are included in list_mental_models for admin visibility."""
|
||||
bank_id = f"test-directive-list-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Set up bank with mission
|
||||
await memory.set_bank_mission(
|
||||
bank_id=bank_id,
|
||||
mission="Test mission",
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Create a directive
|
||||
directive = await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Test Directive",
|
||||
description="A test directive",
|
||||
subtype="directive",
|
||||
observations=[{"title": "Rule", "content": "Follow this rule"}],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Create a pinned model
|
||||
pinned = await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Test Pinned",
|
||||
description="A test pinned model",
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# List without subtype filter - both should appear
|
||||
models = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Both should appear (directives included in API listing for admin visibility)
|
||||
model_ids = [m["id"] for m in models]
|
||||
assert pinned["id"] in model_ids
|
||||
assert directive["id"] in model_ids
|
||||
|
||||
# List with directive subtype filter - should find only directive
|
||||
directives = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
subtype="directive",
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(directives) == 1
|
||||
assert directives[0]["id"] == directive["id"]
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
async def test_directive_get_includes_observations(self, memory: MemoryEngine, request_context):
|
||||
"""Test that getting a directive returns its user-provided observations."""
|
||||
bank_id = f"test-directive-get-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Ensure bank exists
|
||||
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
|
||||
# Create a directive with observations
|
||||
created = await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Meeting Rules",
|
||||
description="Rules for scheduling meetings",
|
||||
subtype="directive",
|
||||
observations=[
|
||||
{"title": "No mornings", "content": "Never schedule meetings before noon"},
|
||||
{"title": "Max duration", "content": "Meetings should be 30 minutes max"},
|
||||
],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Get the directive
|
||||
retrieved = await memory.get_mental_model(
|
||||
bank_id=bank_id,
|
||||
model_id=created["id"],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert retrieved is not None
|
||||
assert retrieved["subtype"] == "directive"
|
||||
assert len(retrieved["observations"]) == 2
|
||||
assert retrieved["observations"][0].title == "No mornings"
|
||||
assert retrieved["observations"][1].title == "Max duration"
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
async def test_directive_survives_refresh(self, memory: MemoryEngine, request_context):
|
||||
"""Test that directives are not modified during refresh_mental_models."""
|
||||
bank_id = f"test-directive-refresh-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Set up bank with mission
|
||||
await memory.set_bank_mission(
|
||||
bank_id=bank_id,
|
||||
mission="Test mission",
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Add some test data
|
||||
await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=[{"content": "Alice is the engineer."}],
|
||||
request_context=request_context,
|
||||
)
|
||||
await memory.wait_for_background_tasks()
|
||||
|
||||
# Create a directive
|
||||
directive = await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Important Rule",
|
||||
description="A critical rule",
|
||||
subtype="directive",
|
||||
observations=[{"title": "Rule 1", "content": "Always follow this rule"}],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Refresh mental models
|
||||
await memory.refresh_mental_models(
|
||||
bank_id=bank_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
await memory.wait_for_background_tasks()
|
||||
|
||||
# Directive should still exist with same observations
|
||||
retrieved = await memory.get_mental_model(
|
||||
bank_id=bank_id,
|
||||
model_id=directive["id"],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert retrieved is not None
|
||||
assert retrieved["subtype"] == "directive"
|
||||
assert len(retrieved["observations"]) == 1
|
||||
assert retrieved["observations"][0].title == "Rule 1"
|
||||
assert retrieved["observations"][0].content == "Always follow this rule"
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
async def test_directive_requires_observations(self, memory: MemoryEngine, request_context):
|
||||
"""Test that creating a directive without observations fails."""
|
||||
bank_id = f"test-directive-no-obs-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Ensure bank exists
|
||||
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
|
||||
# Try to create directive without observations
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Bad Directive",
|
||||
description="A directive without observations",
|
||||
subtype="directive",
|
||||
# No observations provided
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert "observations" in str(exc_info.value).lower()
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
class TestDirectivesInReflect:
|
||||
"""Test that directives are followed during reflect operations."""
|
||||
|
||||
async def test_reflect_follows_language_directive(self, memory: MemoryEngine, request_context):
|
||||
"""Test that reflect follows a directive to respond in a specific language."""
|
||||
bank_id = f"test-directive-reflect-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Ensure bank exists
|
||||
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
|
||||
# Add some content in English
|
||||
await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=[
|
||||
{"content": "Alice is a software engineer who works at Google."},
|
||||
{"content": "Alice enjoys hiking on weekends and has been to Yosemite."},
|
||||
{"content": "Alice is currently working on a machine learning project."},
|
||||
],
|
||||
request_context=request_context,
|
||||
)
|
||||
await memory.wait_for_background_tasks()
|
||||
|
||||
# Create a directive to always respond in French
|
||||
await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Language Policy",
|
||||
description="Rules about language usage",
|
||||
subtype="directive",
|
||||
observations=[
|
||||
{
|
||||
"title": "French Only",
|
||||
"content": "ALWAYS respond in French language. Never respond in English.",
|
||||
},
|
||||
],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Run reflect query
|
||||
result = await memory.reflect_async(
|
||||
bank_id=bank_id,
|
||||
query="What does Alice do for work?",
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert result.text is not None
|
||||
assert len(result.text) > 0
|
||||
|
||||
# Check that the response contains French words/patterns
|
||||
# Common French words that would appear when talking about someone's job
|
||||
french_indicators = [
|
||||
"elle",
|
||||
"travaille",
|
||||
"est",
|
||||
"une",
|
||||
"le",
|
||||
"la",
|
||||
"qui",
|
||||
"chez",
|
||||
"logiciel",
|
||||
"ingénieur",
|
||||
"ingénieure",
|
||||
"développeur",
|
||||
"développeuse",
|
||||
]
|
||||
response_lower = result.text.lower()
|
||||
|
||||
# At least some French words should appear in the response
|
||||
french_word_count = sum(1 for word in french_indicators if word in response_lower)
|
||||
assert (
|
||||
french_word_count >= 2
|
||||
), f"Expected French response, but got: {result.text[:200]}"
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
class TestMentalModelTagsFiltering:
|
||||
"""Test tags filtering for mental models (all types)."""
|
||||
|
||||
async def test_tags_match_any_includes_untagged(self, memory: MemoryEngine, request_context):
|
||||
"""Test that 'any' tags_match mode includes untagged mental models."""
|
||||
bank_id = f"test-mm-tags-any-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Ensure bank exists
|
||||
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
|
||||
# Create an UNTAGGED pinned model
|
||||
await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Global Model",
|
||||
description="A global mental model",
|
||||
subtype="pinned",
|
||||
tags=[], # No tags - should be included with "any" mode
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Test 1: list_mental_models with tags and tags_match="any" should include untagged
|
||||
models_any = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
tags=["some-tag"],
|
||||
tags_match="any", # Should include untagged
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(models_any) == 1, f"Expected untagged model with 'any' mode, got {len(models_any)}"
|
||||
|
||||
# Test 2: list_mental_models with tags and tags_match="any_strict" should exclude untagged
|
||||
models_strict = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
tags=["some-tag"],
|
||||
tags_match="any_strict", # Should exclude untagged
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(models_strict) == 0, f"Expected no models with 'any_strict' mode, got {len(models_strict)}"
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
async def test_tags_match_strict_modes(self, memory: MemoryEngine, request_context):
|
||||
"""Test that strict modes only include mental models with matching tags."""
|
||||
bank_id = f"test-mm-tags-strict-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Ensure bank exists
|
||||
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
|
||||
# Create a TAGGED pinned model
|
||||
await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Tagged Model",
|
||||
description="A tagged mental model",
|
||||
subtype="pinned",
|
||||
tags=["project-a"],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Create an UNTAGGED pinned model
|
||||
await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Untagged Model",
|
||||
description="An untagged mental model",
|
||||
subtype="pinned",
|
||||
tags=[], # No tags
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Test 1: any_strict with matching tag - should get ONLY the tagged model
|
||||
models_match = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
tags=["project-a"],
|
||||
tags_match="any_strict",
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(models_match) == 1, f"Expected 1 model with matching tag, got {len(models_match)}"
|
||||
assert models_match[0]["name"] == "Tagged Model"
|
||||
|
||||
# Test 2: any_strict with different tag - should get NO models
|
||||
models_no_match = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
tags=["project-b"],
|
||||
tags_match="any_strict",
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(models_no_match) == 0, f"Expected no models with non-matching tag, got {len(models_no_match)}"
|
||||
|
||||
# Test 3: any (non-strict) with any tag - should get BOTH models
|
||||
models_any = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
tags=["project-a"],
|
||||
tags_match="any",
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(models_any) == 2, f"Expected 2 models with 'any' mode, got {len(models_any)}"
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
async def test_tags_match_all_strict(self, memory: MemoryEngine, request_context):
|
||||
"""Test that 'all_strict' requires ALL tags to be present."""
|
||||
bank_id = f"test-mm-tags-all-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Ensure bank exists
|
||||
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
|
||||
# Create a model with multiple tags
|
||||
await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Multi-Tag Model",
|
||||
description="Has project-a and project-b tags",
|
||||
subtype="pinned",
|
||||
tags=["project-a", "project-b"],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Create a model with only one tag
|
||||
await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Single-Tag Model",
|
||||
description="Has only project-a tag",
|
||||
subtype="pinned",
|
||||
tags=["project-a"],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Test 1: all_strict with both tags - should get ONLY the multi-tag model
|
||||
models_all = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
tags=["project-a", "project-b"],
|
||||
tags_match="all_strict",
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(models_all) == 1, f"Expected 1 model with all tags, got {len(models_all)}"
|
||||
assert models_all[0]["name"] == "Multi-Tag Model"
|
||||
|
||||
# Test 2: all (non-strict) with both tags - should include untagged too
|
||||
# Add an untagged model
|
||||
await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Untagged Model",
|
||||
description="No tags",
|
||||
subtype="pinned",
|
||||
tags=[],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
models_all_non_strict = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
tags=["project-a", "project-b"],
|
||||
tags_match="all",
|
||||
request_context=request_context,
|
||||
)
|
||||
# Should get Multi-Tag Model + Untagged Model
|
||||
assert len(models_all_non_strict) == 2, f"Expected 2 models with 'all' mode, got {len(models_all_non_strict)}"
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
class TestDirectivesPromptInjection:
|
||||
"""Test that directives are properly injected into the system prompt."""
|
||||
|
||||
def test_build_directives_section_empty(self):
|
||||
"""Test that empty directives returns empty string."""
|
||||
from hindsight_api.engine.reflect.prompts import build_directives_section
|
||||
|
||||
result = build_directives_section([])
|
||||
assert result == ""
|
||||
|
||||
def test_build_directives_section_with_observations(self):
|
||||
"""Test that directives with observations are formatted correctly."""
|
||||
from hindsight_api.engine.reflect.prompts import build_directives_section
|
||||
|
||||
directives = [
|
||||
{
|
||||
"name": "Competitor Policy",
|
||||
"observations": [
|
||||
{"title": "Never mention", "content": "Never mention competitor names"},
|
||||
{"title": "Redirect", "content": "Redirect to our features"},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
result = build_directives_section(directives)
|
||||
|
||||
assert "## DIRECTIVES (MANDATORY)" in result
|
||||
assert "**Never mention**: Never mention competitor names" in result
|
||||
assert "**Redirect**: Redirect to our features" in result
|
||||
assert "NEVER violate these directives" in result
|
||||
|
||||
def test_build_directives_section_fallback_to_description(self):
|
||||
"""Test that directives without observations fall back to description."""
|
||||
from hindsight_api.engine.reflect.prompts import build_directives_section
|
||||
|
||||
directives = [
|
||||
{
|
||||
"name": "Simple Rule",
|
||||
"description": "Just a simple rule description",
|
||||
"observations": [],
|
||||
}
|
||||
]
|
||||
|
||||
result = build_directives_section(directives)
|
||||
|
||||
assert "**Simple Rule**: Just a simple rule description" in result
|
||||
|
||||
def test_system_prompt_includes_directives(self):
|
||||
"""Test that build_system_prompt_for_tools includes directives."""
|
||||
from hindsight_api.engine.reflect.prompts import build_system_prompt_for_tools
|
||||
|
||||
bank_profile = {"name": "Test Bank", "mission": "Test mission"}
|
||||
directives = [
|
||||
{
|
||||
"name": "Test Directive",
|
||||
"observations": [{"title": "Rule", "content": "Follow this rule"}],
|
||||
}
|
||||
]
|
||||
|
||||
prompt = build_system_prompt_for_tools(
|
||||
bank_profile=bank_profile,
|
||||
directives=directives,
|
||||
)
|
||||
|
||||
assert "## DIRECTIVES (MANDATORY)" in prompt
|
||||
assert "**Rule**: Follow this rule" in prompt
|
||||
# Directives should appear before CRITICAL RULES
|
||||
directives_pos = prompt.find("## DIRECTIVES")
|
||||
critical_rules_pos = prompt.find("## CRITICAL RULES")
|
||||
assert directives_pos < critical_rules_pos
|
||||
|
||||
|
||||
class TestMentalModelVersioning:
|
||||
"""Test mental model versioning functionality."""
|
||||
|
||||
async def test_refresh_creates_version(self, memory_with_mission, request_context):
|
||||
"""Test that refreshing a mental model creates a version entry."""
|
||||
memory, bank_id = memory_with_mission
|
||||
|
||||
# First create a mental model via refresh_mental_models
|
||||
await memory.refresh_mental_models(
|
||||
bank_id=bank_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
await memory.wait_for_background_tasks()
|
||||
|
||||
# Get the created models
|
||||
models = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(models) > 0
|
||||
|
||||
model_id = models[0]["id"]
|
||||
|
||||
# Refresh the specific model to trigger versioning
|
||||
result = await memory.refresh_mental_model(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
# Version should be incremented
|
||||
assert result.get("version", 0) >= 1
|
||||
|
||||
# Check version history
|
||||
versions = await memory.get_mental_model_versions(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert len(versions) >= 1
|
||||
assert versions[0]["version"] >= 1
|
||||
assert "created_at" in versions[0]
|
||||
assert "observation_count" in versions[0]
|
||||
|
||||
async def test_get_specific_version(self, memory_with_mission, request_context):
|
||||
"""Test retrieving a specific version of a mental model."""
|
||||
memory, bank_id = memory_with_mission
|
||||
|
||||
# Create and refresh a mental model
|
||||
await memory.refresh_mental_models(
|
||||
bank_id=bank_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
await memory.wait_for_background_tasks()
|
||||
|
||||
models = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(models) > 0
|
||||
|
||||
model_id = models[0]["id"]
|
||||
|
||||
# Refresh to create version
|
||||
await memory.refresh_mental_model(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Get versions
|
||||
versions = await memory.get_mental_model_versions(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(versions) >= 1
|
||||
|
||||
# Get specific version
|
||||
version_num = versions[0]["version"]
|
||||
version_data = await memory.get_mental_model_version(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
version=version_num,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert version_data is not None
|
||||
assert version_data["version"] == version_num
|
||||
assert "observations" in version_data
|
||||
|
||||
async def test_version_cleanup_keeps_max_versions(self, memory_with_mission, request_context):
|
||||
"""Test that old versions are cleaned up when max is exceeded."""
|
||||
memory, bank_id = memory_with_mission
|
||||
|
||||
# Create a mental model
|
||||
await memory.refresh_mental_models(
|
||||
bank_id=bank_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
await memory.wait_for_background_tasks()
|
||||
|
||||
models = await memory.list_mental_models(
|
||||
bank_id=bank_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
assert len(models) > 0
|
||||
|
||||
model_id = models[0]["id"]
|
||||
|
||||
# Refresh multiple times to create versions
|
||||
for _ in range(3):
|
||||
await memory.refresh_mental_model(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Get versions - should have multiple but within max limit
|
||||
versions = await memory.get_mental_model_versions(
|
||||
bank_id=bank_id,
|
||||
model_id=model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Should have versions (exact count depends on config, but at least some)
|
||||
assert len(versions) >= 1
|
||||
# Versions should be in descending order
|
||||
if len(versions) > 1:
|
||||
assert versions[0]["version"] > versions[1]["version"]
|
||||
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
"""Tests for observation trend computation and evidence-grounded models."""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api.engine.reflect.observations import (
|
||||
CandidateObservation,
|
||||
Observation,
|
||||
ObservationEvidence,
|
||||
Trend,
|
||||
compute_trend,
|
||||
verify_evidence_quotes,
|
||||
)
|
||||
|
||||
|
||||
class TestComputeTrend:
|
||||
"""Tests for the compute_trend function."""
|
||||
|
||||
def test_empty_evidence_returns_stale(self):
|
||||
"""No evidence should return STALE trend."""
|
||||
trend = compute_trend([])
|
||||
assert trend == Trend.STALE
|
||||
|
||||
def test_all_recent_evidence_returns_new(self):
|
||||
"""All evidence within recent window (30 days) should return NEW trend.
|
||||
|
||||
Scenario: User just started using the app and mentioned they like coffee twice.
|
||||
Both mentions are within the last 2 weeks, so this is a NEW observation.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
evidence = [
|
||||
ObservationEvidence(
|
||||
memory_id="mem-coffee-morning",
|
||||
quote="I always start my day with a large black coffee",
|
||||
relevance="Shows preference for coffee and morning routine",
|
||||
timestamp=now - timedelta(days=5),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-coffee-meeting",
|
||||
quote="grabbed coffee before the standup meeting",
|
||||
relevance="Confirms regular coffee consumption",
|
||||
timestamp=now - timedelta(days=10),
|
||||
),
|
||||
]
|
||||
|
||||
trend = compute_trend(evidence, now=now)
|
||||
assert trend == Trend.NEW
|
||||
|
||||
def test_no_recent_evidence_returns_stale(self):
|
||||
"""No evidence in recent window should return STALE trend.
|
||||
|
||||
Scenario: User mentioned running 3 months ago but hasn't mentioned it since.
|
||||
The observation about running as a hobby may no longer be accurate.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
evidence = [
|
||||
ObservationEvidence(
|
||||
memory_id="mem-running-march",
|
||||
quote="training for a half marathon in the spring",
|
||||
relevance="Shows interest in running",
|
||||
timestamp=now - timedelta(days=60),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-running-feb",
|
||||
quote="went for a 10k run this morning",
|
||||
relevance="Active runner",
|
||||
timestamp=now - timedelta(days=100),
|
||||
),
|
||||
]
|
||||
|
||||
trend = compute_trend(evidence, now=now)
|
||||
assert trend == Trend.STALE
|
||||
|
||||
def test_stable_evidence_distribution(self):
|
||||
"""Evidence spread evenly across time should return STABLE trend.
|
||||
|
||||
Scenario: User has consistently mentioned working remotely over 4 months.
|
||||
Evidence is well-distributed, indicating a stable, ongoing preference.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
evidence = [
|
||||
# Recent (within 30 days)
|
||||
ObservationEvidence(
|
||||
memory_id="mem-remote-jan",
|
||||
quote="working from my home office today",
|
||||
relevance="Current remote work",
|
||||
timestamp=now - timedelta(days=5),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-remote-dec",
|
||||
quote="the flexibility of remote work is great",
|
||||
relevance="Values remote work",
|
||||
timestamp=now - timedelta(days=15),
|
||||
),
|
||||
# Middle period (30-90 days)
|
||||
ObservationEvidence(
|
||||
memory_id="mem-remote-nov",
|
||||
quote="set up a standing desk at home",
|
||||
relevance="Invested in home office",
|
||||
timestamp=now - timedelta(days=45),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-remote-oct",
|
||||
quote="prefer async communication over meetings",
|
||||
relevance="Remote work style preference",
|
||||
timestamp=now - timedelta(days=60),
|
||||
),
|
||||
# Older (90+ days)
|
||||
ObservationEvidence(
|
||||
memory_id="mem-remote-sep",
|
||||
quote="switched to fully remote last quarter",
|
||||
relevance="Original transition to remote",
|
||||
timestamp=now - timedelta(days=100),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-remote-aug",
|
||||
quote="negotiated remote work in my new contract",
|
||||
relevance="Intentional choice for remote",
|
||||
timestamp=now - timedelta(days=120),
|
||||
),
|
||||
]
|
||||
|
||||
trend = compute_trend(evidence, now=now)
|
||||
assert trend == Trend.STABLE
|
||||
|
||||
def test_strengthening_trend(self):
|
||||
"""Much more recent evidence than older should return STRENGTHENING trend.
|
||||
|
||||
Scenario: User has been increasingly talking about learning Python recently
|
||||
after mentioning it once months ago. Interest appears to be growing.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
evidence = [
|
||||
# Lots of recent evidence - actively learning
|
||||
ObservationEvidence(
|
||||
memory_id="mem-python-project",
|
||||
quote="finished my first Python project - a web scraper",
|
||||
relevance="Completed Python project",
|
||||
timestamp=now - timedelta(days=2),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-python-course",
|
||||
quote="halfway through the Python bootcamp",
|
||||
relevance="Active learning",
|
||||
timestamp=now - timedelta(days=5),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-python-book",
|
||||
quote="reading Fluent Python, it's excellent",
|
||||
relevance="Deepening knowledge",
|
||||
timestamp=now - timedelta(days=10),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-python-practice",
|
||||
quote="solved 50 LeetCode problems in Python",
|
||||
relevance="Practicing skills",
|
||||
timestamp=now - timedelta(days=15),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-python-ide",
|
||||
quote="set up VS Code with all the Python extensions",
|
||||
relevance="Setting up environment",
|
||||
timestamp=now - timedelta(days=20),
|
||||
),
|
||||
# Only one old mention - initial interest
|
||||
ObservationEvidence(
|
||||
memory_id="mem-python-start",
|
||||
quote="thinking about learning Python someday",
|
||||
relevance="Initial interest",
|
||||
timestamp=now - timedelta(days=100),
|
||||
),
|
||||
]
|
||||
|
||||
trend = compute_trend(evidence, now=now)
|
||||
assert trend == Trend.STRENGTHENING
|
||||
|
||||
def test_weakening_trend(self):
|
||||
"""Much less recent evidence than older should return WEAKENING trend.
|
||||
|
||||
Scenario: User was very active in a book club last year but mentions
|
||||
have tapered off. The observation about being a book club member
|
||||
may be becoming less relevant.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
evidence = [
|
||||
# Only one recent mention
|
||||
ObservationEvidence(
|
||||
memory_id="mem-book-recent",
|
||||
quote="haven't had time for book club lately",
|
||||
relevance="Reduced participation",
|
||||
timestamp=now - timedelta(days=10),
|
||||
),
|
||||
# Lots of older evidence - was very active
|
||||
ObservationEvidence(
|
||||
memory_id="mem-book-aug",
|
||||
quote="hosting book club at my place next week",
|
||||
relevance="Active organizer",
|
||||
timestamp=now - timedelta(days=40),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-book-july",
|
||||
quote="leading the discussion on 1984",
|
||||
relevance="Active participant",
|
||||
timestamp=now - timedelta(days=50),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-book-june",
|
||||
quote="we picked The Midnight Library for June",
|
||||
relevance="Regular member",
|
||||
timestamp=now - timedelta(days=60),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-book-may",
|
||||
quote="book club was amazing tonight",
|
||||
relevance="Enthusiastic member",
|
||||
timestamp=now - timedelta(days=100),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-book-april",
|
||||
quote="joined a new book club in my neighborhood",
|
||||
relevance="Started participation",
|
||||
timestamp=now - timedelta(days=110),
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-book-march",
|
||||
quote="excited to finally join a book club",
|
||||
relevance="Initial enthusiasm",
|
||||
timestamp=now - timedelta(days=120),
|
||||
),
|
||||
]
|
||||
|
||||
trend = compute_trend(evidence, now=now)
|
||||
assert trend == Trend.WEAKENING
|
||||
|
||||
|
||||
class TestObservationModel:
|
||||
"""Tests for the Observation model."""
|
||||
|
||||
def test_observation_computed_trend(self):
|
||||
"""Observation should have computed trend property based on evidence."""
|
||||
now = datetime.now(timezone.utc)
|
||||
obs = Observation(
|
||||
title="Morning meeting preference",
|
||||
content="Prefers morning meetings over afternoon ones",
|
||||
evidence=[
|
||||
ObservationEvidence(
|
||||
memory_id="mem-morning-standup",
|
||||
quote="I'm most productive in morning meetings",
|
||||
relevance="Direct preference statement",
|
||||
timestamp=now - timedelta(days=5),
|
||||
),
|
||||
],
|
||||
created_at=now,
|
||||
)
|
||||
|
||||
assert obs.trend == Trend.NEW
|
||||
assert obs.evidence_count == 1
|
||||
|
||||
def test_observation_evidence_span(self):
|
||||
"""Observation should compute evidence span correctly.
|
||||
|
||||
The span shows the date range of supporting evidence, helping
|
||||
understand how long this pattern has been observed.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
old_time = now - timedelta(days=100)
|
||||
recent_time = now - timedelta(days=5)
|
||||
|
||||
obs = Observation(
|
||||
title="Values work-life balance",
|
||||
content="Values work-life balance highly",
|
||||
evidence=[
|
||||
ObservationEvidence(
|
||||
memory_id="mem-balance-old",
|
||||
quote="turned down a promotion because of the hours",
|
||||
relevance="Prioritized balance over advancement",
|
||||
timestamp=old_time,
|
||||
),
|
||||
ObservationEvidence(
|
||||
memory_id="mem-balance-recent",
|
||||
quote="always log off by 6pm no matter what",
|
||||
relevance="Maintains boundaries",
|
||||
timestamp=recent_time,
|
||||
),
|
||||
],
|
||||
created_at=now,
|
||||
)
|
||||
|
||||
evidence_span = obs.evidence_span
|
||||
assert evidence_span["from"] == old_time.isoformat()
|
||||
assert evidence_span["to"] == recent_time.isoformat()
|
||||
|
||||
def test_observation_empty_evidence_span(self):
|
||||
"""Observation with no evidence should have null span."""
|
||||
obs = Observation(
|
||||
title="Test observation",
|
||||
content="Test observation without evidence",
|
||||
evidence=[],
|
||||
)
|
||||
|
||||
evidence_span = obs.evidence_span
|
||||
assert evidence_span["from"] is None
|
||||
assert evidence_span["to"] is None
|
||||
|
||||
|
||||
class TestVerifyEvidenceQuotes:
|
||||
"""Tests for evidence quote verification.
|
||||
|
||||
This ensures the LLM isn't hallucinating quotes - every quote
|
||||
must actually appear in the source memory.
|
||||
"""
|
||||
|
||||
def test_valid_quotes(self):
|
||||
"""Should return True when quotes exist in their source memories."""
|
||||
obs = Observation(
|
||||
title="Enjoys hiking",
|
||||
content="Enjoys hiking on weekends",
|
||||
evidence=[
|
||||
ObservationEvidence(
|
||||
memory_id="mem-hiking-trip",
|
||||
quote="went hiking at Mount Tam",
|
||||
relevance="Shows hiking activity",
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
memories = {
|
||||
"mem-hiking-trip": "Had a great Saturday - went hiking at Mount Tam with friends and saw amazing views."
|
||||
}
|
||||
is_valid, errors = verify_evidence_quotes(obs, memories)
|
||||
|
||||
assert is_valid is True
|
||||
assert len(errors) == 0
|
||||
|
||||
def test_invalid_quote(self):
|
||||
"""Should return False when quote doesn't exist in memory.
|
||||
|
||||
This catches LLM hallucinations where it fabricates quotes.
|
||||
"""
|
||||
obs = Observation(
|
||||
title="Loves spicy food",
|
||||
content="Loves spicy food",
|
||||
evidence=[
|
||||
ObservationEvidence(
|
||||
memory_id="mem-dinner",
|
||||
quote="I love extra hot salsa",
|
||||
relevance="Shows spicy food preference",
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
memories = {"mem-dinner": "Had tacos for dinner. The guacamole was really fresh."}
|
||||
is_valid, errors = verify_evidence_quotes(obs, memories)
|
||||
|
||||
assert is_valid is False
|
||||
assert len(errors) == 1
|
||||
assert "Quote not found" in errors[0]
|
||||
|
||||
def test_missing_memory(self):
|
||||
"""Should return False when referenced memory doesn't exist.
|
||||
|
||||
This catches cases where the LLM references a memory ID that
|
||||
was never actually retrieved.
|
||||
"""
|
||||
obs = Observation(
|
||||
title="Has a dog named Max",
|
||||
content="Has a dog named Max",
|
||||
evidence=[
|
||||
ObservationEvidence(
|
||||
memory_id="mem-pet-story",
|
||||
quote="took Max to the vet",
|
||||
relevance="Shows pet ownership",
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
memories = {"mem-different-id": "Some unrelated memory content"}
|
||||
is_valid, errors = verify_evidence_quotes(obs, memories)
|
||||
|
||||
assert is_valid is False
|
||||
assert len(errors) == 1
|
||||
assert "not found" in errors[0]
|
||||
|
||||
|
||||
class TestCandidateObservation:
|
||||
"""Tests for candidate observation model.
|
||||
|
||||
Candidates are generated in the SEED phase and validated
|
||||
before becoming full observations.
|
||||
"""
|
||||
|
||||
def test_create_candidate(self):
|
||||
"""Should create candidate with content and seed memories."""
|
||||
candidate = CandidateObservation(
|
||||
content="User prefers async communication over meetings",
|
||||
seed_memory_ids=["mem-slack-pref", "mem-meeting-decline"],
|
||||
)
|
||||
|
||||
assert candidate.content == "User prefers async communication over meetings"
|
||||
assert len(candidate.seed_memory_ids) == 2
|
||||
assert "mem-slack-pref" in candidate.seed_memory_ids
|
||||
@@ -88,19 +88,7 @@ class TestToolLookup:
|
||||
"subtype": "learned",
|
||||
"name": "Model 1",
|
||||
"description": "First model",
|
||||
"observations": {
|
||||
"observations": [
|
||||
{
|
||||
"title": "Overview",
|
||||
"content": "Full summary of model 1",
|
||||
"evidence": [
|
||||
{"memory_id": "mem-1", "quote": "quote 1", "relevance": "relevant", "timestamp": "2024-01-01T00:00:00Z"},
|
||||
{"memory_id": "mem-2", "quote": "quote 2", "relevance": "relevant", "timestamp": "2024-01-01T00:00:00Z"},
|
||||
],
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
]
|
||||
},
|
||||
"observations": {"observations": [{"title": "Overview", "text": "Full summary of model 1", "memory_ids": ["mem-1", "mem-2"]}]},
|
||||
"entity_id": None,
|
||||
"last_updated": MagicMock(isoformat=lambda: "2024-01-01T00:00:00"),
|
||||
}
|
||||
@@ -110,12 +98,9 @@ class TestToolLookup:
|
||||
assert result["found"] is True
|
||||
assert result["model"]["id"] == "model-1"
|
||||
assert len(result["model"]["observations"]) == 1
|
||||
# Observations are now Observation objects
|
||||
obs = result["model"]["observations"][0]
|
||||
assert obs.content == "Full summary of model 1"
|
||||
assert obs.title == "Overview"
|
||||
assert len(obs.evidence) == 2
|
||||
assert obs.evidence[0].memory_id == "mem-1"
|
||||
assert result["model"]["observations"][0]["text"] == "Full summary of model 1"
|
||||
# Verify memory_ids are mapped to based_on
|
||||
assert result["model"]["observations"][0]["based_on"] == ["mem-1", "mem-2"]
|
||||
|
||||
async def test_model_not_found(self, mock_conn):
|
||||
"""Test looking up non-existent model."""
|
||||
@@ -855,158 +840,6 @@ class TestReflectAgent:
|
||||
|
||||
assert result.text == "The answer is simple and direct."
|
||||
|
||||
async def test_agent_includes_directives_in_system_prompt(self, mock_llm, bank_profile, mock_tools):
|
||||
"""Test that directives are included in the system prompt."""
|
||||
from hindsight_api.engine.reflect.observations import Observation
|
||||
|
||||
# Create directive with Observation objects (new format)
|
||||
directives = [
|
||||
{
|
||||
"id": "response-rules",
|
||||
"name": "Response Rules",
|
||||
"description": "Rules for responses",
|
||||
"subtype": "directive",
|
||||
"observations": [
|
||||
Observation(
|
||||
title="No Speculation",
|
||||
content="Never speculate about information not in the memories.",
|
||||
evidence=[],
|
||||
),
|
||||
Observation(
|
||||
title="Be Concise",
|
||||
content="Always keep responses under 100 words.",
|
||||
evidence=[],
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
# Capture the system prompt
|
||||
captured_messages = []
|
||||
|
||||
async def capture_call(*args, **kwargs):
|
||||
if "messages" in kwargs:
|
||||
captured_messages.extend(kwargs["messages"])
|
||||
return self._make_tool_result([{"name": "done", "arguments": {"answer": "Done."}}])
|
||||
|
||||
mock_llm.call_with_tools.side_effect = [
|
||||
# First: gather evidence (guardrail requirement)
|
||||
self._make_tool_result([{"name": "recall", "arguments": {"query": "test"}}]),
|
||||
# Then: done
|
||||
self._make_tool_result([{"name": "done", "arguments": {"answer": "Done."}}]),
|
||||
]
|
||||
|
||||
# Store original to check messages
|
||||
original_call = mock_llm.call_with_tools
|
||||
|
||||
async def wrapped_call(*args, **kwargs):
|
||||
if "messages" in kwargs:
|
||||
captured_messages.extend(kwargs["messages"])
|
||||
return await original_call(*args, **kwargs)
|
||||
|
||||
mock_llm.call_with_tools = wrapped_call
|
||||
|
||||
result = await run_reflect_agent(
|
||||
llm_config=mock_llm,
|
||||
bank_id="test-bank",
|
||||
query="What do we know?",
|
||||
bank_profile=bank_profile,
|
||||
directives=directives,
|
||||
**mock_tools,
|
||||
)
|
||||
|
||||
# Find the system message
|
||||
system_messages = [m for m in captured_messages if m.get("role") == "system"]
|
||||
assert len(system_messages) > 0, "No system message found"
|
||||
|
||||
system_content = system_messages[0]["content"]
|
||||
|
||||
# Verify directives are in the system prompt
|
||||
assert "DIRECTIVES" in system_content, "Directives section not found in system prompt"
|
||||
assert "No Speculation" in system_content, "Directive title not found"
|
||||
assert "Never speculate" in system_content, "Directive content not found"
|
||||
assert "Be Concise" in system_content, "Second directive title not found"
|
||||
assert "100 words" in system_content, "Second directive content not found"
|
||||
assert "NEVER violate these directives" in system_content, "Directive warning not found"
|
||||
|
||||
|
||||
class TestDirectivesSection:
|
||||
"""Test the directives section builder."""
|
||||
|
||||
def test_build_directives_section_with_observation_objects(self):
|
||||
"""Test building directives section with Observation objects."""
|
||||
from hindsight_api.engine.reflect.observations import Observation
|
||||
from hindsight_api.engine.reflect.prompts import build_directives_section
|
||||
|
||||
directives = [
|
||||
{
|
||||
"name": "Safety Rules",
|
||||
"observations": [
|
||||
Observation(
|
||||
title="No Harmful Content",
|
||||
content="Never generate harmful or dangerous content.",
|
||||
evidence=[],
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
result = build_directives_section(directives)
|
||||
|
||||
assert "DIRECTIVES" in result
|
||||
assert "No Harmful Content" in result
|
||||
assert "Never generate harmful" in result
|
||||
assert "NEVER violate" in result
|
||||
|
||||
def test_build_directives_section_with_dicts(self):
|
||||
"""Test building directives section with dict observations."""
|
||||
from hindsight_api.engine.reflect.prompts import build_directives_section
|
||||
|
||||
directives = [
|
||||
{
|
||||
"name": "Safety Rules",
|
||||
"observations": [
|
||||
{
|
||||
"title": "No Harmful Content",
|
||||
"content": "Never generate harmful or dangerous content.",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
result = build_directives_section(directives)
|
||||
|
||||
assert "DIRECTIVES" in result
|
||||
assert "No Harmful Content" in result
|
||||
assert "Never generate harmful" in result
|
||||
|
||||
def test_build_directives_section_fallback_to_description(self):
|
||||
"""Test that directives without observations use description."""
|
||||
from hindsight_api.engine.reflect.prompts import build_directives_section
|
||||
|
||||
directives = [
|
||||
{
|
||||
"name": "Simple Rule",
|
||||
"description": "This is a simple rule to follow.",
|
||||
"observations": [],
|
||||
},
|
||||
]
|
||||
|
||||
result = build_directives_section(directives)
|
||||
|
||||
assert "Simple Rule" in result
|
||||
assert "simple rule to follow" in result
|
||||
|
||||
def test_build_directives_section_empty(self):
|
||||
"""Test that empty directives returns empty string."""
|
||||
from hindsight_api.engine.reflect.prompts import build_directives_section
|
||||
|
||||
result = build_directives_section([])
|
||||
assert result == ""
|
||||
|
||||
result = build_directives_section(None)
|
||||
assert result == ""
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestReflectIntegration:
|
||||
|
||||
@@ -2058,26 +2058,3 @@ async def test_user_provided_entities(memory, request_context):
|
||||
|
||||
finally:
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
def test_recall_result_model_empty_construction():
|
||||
"""
|
||||
Test that RecallResultModel can be constructed with empty results.
|
||||
|
||||
This is a regression test for the bug where constructing an empty RecallResultModel
|
||||
would cause an UnboundLocalError because RecallResult was imported as RecallResultModel
|
||||
but the code mistakenly used the wrong name.
|
||||
|
||||
The fix ensures RecallResultModel is used consistently throughout memory_engine.py.
|
||||
"""
|
||||
from hindsight_api.engine.response_models import RecallResult
|
||||
|
||||
# This should not raise any errors
|
||||
result = RecallResult(results=[], entities={}, chunks={})
|
||||
|
||||
assert result is not None, "Should create a result object"
|
||||
assert result.results == [], "Should have empty results"
|
||||
assert result.entities == {}, "Should have empty entities"
|
||||
assert result.chunks == {}, "Should have empty chunks"
|
||||
|
||||
logger.info("✓ RecallResult empty construction works correctly")
|
||||
|
||||
@@ -257,7 +257,6 @@ from hindsight_api.extensions import (
|
||||
RetainContext,
|
||||
RecallContext,
|
||||
ReflectContext,
|
||||
RefreshMentalModelContext,
|
||||
)
|
||||
|
||||
|
||||
@@ -289,6 +288,3 @@ class MockOperationValidator(OperationValidatorExtension):
|
||||
|
||||
async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
|
||||
return ValidationResult.accept()
|
||||
|
||||
async def validate_refresh_mental_model(self, ctx: RefreshMentalModelContext) -> ValidationResult:
|
||||
return ValidationResult.accept()
|
||||
|
||||
@@ -55,7 +55,6 @@ pub struct MemoryPutResult {
|
||||
pub items_count: i64,
|
||||
pub message: String,
|
||||
pub is_async: bool,
|
||||
pub operation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -163,54 +162,10 @@ impl ApiClient {
|
||||
items_count: result.items_count,
|
||||
message: format!("Stored {} memory units", result.items_count),
|
||||
is_async: result.async_,
|
||||
operation_id: result.operation_id,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Poll an operation until it completes or fails.
|
||||
/// Returns Ok(true) if completed successfully, Ok(false) if failed, Err if polling error.
|
||||
pub fn poll_operation(&self, agent_id: &str, operation_id: &str, verbose: bool) -> Result<(bool, Option<String>)> {
|
||||
self.runtime.block_on(async {
|
||||
loop {
|
||||
let response = self.client.list_operations(agent_id, None).await?;
|
||||
let ops = response.into_inner();
|
||||
|
||||
// Find our operation
|
||||
let op = ops.operations.iter().find(|o| o.id == operation_id);
|
||||
|
||||
match op {
|
||||
Some(operation) => {
|
||||
if verbose {
|
||||
eprintln!("Operation {} status: {}", operation_id, operation.status);
|
||||
}
|
||||
match operation.status.as_str() {
|
||||
"pending" => {
|
||||
// Still running, wait and poll again
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
"completed" => {
|
||||
// Operation completed successfully
|
||||
return Ok((true, None));
|
||||
}
|
||||
"failed" => {
|
||||
return Ok((false, operation.error_message.clone()));
|
||||
}
|
||||
_ => {
|
||||
// Unknown status, treat as failed
|
||||
return Ok((false, Some(format!("Unknown status: {}", operation.status))));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Operation not in list means it completed successfully (removed from pending/failed)
|
||||
return Ok((true, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_memory(&self, _agent_id: &str, _unit_id: &str, _verbose: bool) -> Result<types::DeleteResponse> {
|
||||
// Note: Individual memory deletion is no longer supported in the API
|
||||
anyhow::bail!("Individual memory deletion is no longer supported. Use 'memory clear' to clear all memories.")
|
||||
@@ -327,105 +282,3 @@ pub use types::{
|
||||
ReflectResponse,
|
||||
RetainRequest,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_operation_deserialize() {
|
||||
let json = r#"{
|
||||
"id": "test-op-123",
|
||||
"task_type": "retain",
|
||||
"items_count": 5,
|
||||
"document_id": "doc-456",
|
||||
"created_at": "2024-01-15T10:00:00Z",
|
||||
"status": "pending",
|
||||
"error_message": null
|
||||
}"#;
|
||||
let op: Operation = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(op.id, "test-op-123");
|
||||
assert_eq!(op.task_type, "retain");
|
||||
assert_eq!(op.items_count, 5);
|
||||
assert_eq!(op.document_id, Some("doc-456".to_string()));
|
||||
assert_eq!(op.status, "pending");
|
||||
assert!(op.error_message.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_operation_deserialize_with_error() {
|
||||
let json = r#"{
|
||||
"id": "test-op-456",
|
||||
"task_type": "retain",
|
||||
"items_count": 3,
|
||||
"document_id": null,
|
||||
"created_at": "2024-01-15T10:00:00Z",
|
||||
"status": "failed",
|
||||
"error_message": "Something went wrong"
|
||||
}"#;
|
||||
let op: Operation = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(op.status, "failed");
|
||||
assert_eq!(op.error_message, Some("Something went wrong".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_put_result_serialize() {
|
||||
let result = MemoryPutResult {
|
||||
success: true,
|
||||
items_count: 10,
|
||||
message: "Stored 10 memory units".to_string(),
|
||||
is_async: true,
|
||||
operation_id: Some("op-789".to_string()),
|
||||
};
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
assert!(json.contains("\"success\":true"));
|
||||
assert!(json.contains("\"items_count\":10"));
|
||||
assert!(json.contains("\"is_async\":true"));
|
||||
assert!(json.contains("\"operation_id\":\"op-789\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_put_result_without_operation_id() {
|
||||
let result = MemoryPutResult {
|
||||
success: true,
|
||||
items_count: 5,
|
||||
message: "Stored 5 memory units".to_string(),
|
||||
is_async: false,
|
||||
operation_id: None,
|
||||
};
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
assert!(json.contains("\"operation_id\":null"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_operations_response_deserialize() {
|
||||
let json = r#"{
|
||||
"bank_id": "test-bank",
|
||||
"operations": [
|
||||
{
|
||||
"id": "op-1",
|
||||
"task_type": "retain",
|
||||
"items_count": 2,
|
||||
"document_id": null,
|
||||
"created_at": "2024-01-15T10:00:00Z",
|
||||
"status": "pending",
|
||||
"error_message": null
|
||||
},
|
||||
{
|
||||
"id": "op-2",
|
||||
"task_type": "retain",
|
||||
"items_count": 3,
|
||||
"document_id": "doc-123",
|
||||
"created_at": "2024-01-15T11:00:00Z",
|
||||
"status": "completed",
|
||||
"error_message": null
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
let ops: OperationsResponse = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(ops.bank_id, "test-bank");
|
||||
assert_eq!(ops.operations.len(), 2);
|
||||
assert_eq!(ops.operations[0].status, "pending");
|
||||
assert_eq!(ops.operations[1].status, "completed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,17 +21,6 @@ fn parse_budget(budget: &str) -> Budget {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a file has a text-based extension
|
||||
fn is_text_file(path: &std::path::Path) -> bool {
|
||||
const TEXT_EXTENSIONS: &[&str] = &[
|
||||
"txt", "md", "json", "yaml", "yml", "toml", "xml", "csv", "log", "rst", "adoc",
|
||||
];
|
||||
path.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.map(|ext| TEXT_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn recall(
|
||||
client: &ApiClient,
|
||||
agent_id: &str,
|
||||
@@ -240,23 +229,29 @@ pub fn retain_files(
|
||||
.filter(|e| e.file_type().is_file())
|
||||
{
|
||||
let path = entry.path();
|
||||
if is_text_file(&path) {
|
||||
files.push(path.to_path_buf());
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "txt" || ext == "md" {
|
||||
files.push(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for entry in fs::read_dir(&path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && is_text_file(&path) {
|
||||
files.push(path);
|
||||
if path.is_file() {
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "txt" || ext == "md" {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if files.is_empty() {
|
||||
ui::print_warning("No text files found (supported: txt, md, json, yaml, yml, toml, xml, csv, log, rst, adoc)");
|
||||
ui::print_warning("No .txt or .md files found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -291,20 +286,19 @@ pub fn retain_files(
|
||||
|
||||
pb.finish_with_message("Files processed");
|
||||
|
||||
// Always use async mode for the API call
|
||||
let request = RetainRequest {
|
||||
items,
|
||||
async_: true,
|
||||
document_tags: None,
|
||||
};
|
||||
|
||||
let spinner = if output_format == OutputFormat::Pretty {
|
||||
Some(ui::create_spinner("Submitting retain request..."))
|
||||
Some(ui::create_spinner("Retaining memories..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let response = client.retain(agent_id, &request, true, verbose);
|
||||
let request = RetainRequest {
|
||||
items,
|
||||
async_: r#async,
|
||||
document_tags: None,
|
||||
};
|
||||
|
||||
let response = client.retain(agent_id, &request, r#async, verbose);
|
||||
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
@@ -312,55 +306,16 @@ pub fn retain_files(
|
||||
|
||||
match response {
|
||||
Ok(result) => {
|
||||
if r#async {
|
||||
// User requested async mode - return immediately
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_success("Files queued for processing");
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_success("Files retained successfully");
|
||||
if result.is_async {
|
||||
println!(" Status: queued for background processing");
|
||||
println!(" Items: {}", result.items_count);
|
||||
if let Some(op_id) = &result.operation_id {
|
||||
println!(" Operation ID: {}", op_id);
|
||||
}
|
||||
} else {
|
||||
output::print_output(&result, output_format)?;
|
||||
println!(" Total units created: {}", result.items_count);
|
||||
}
|
||||
} else {
|
||||
// Poll until completion
|
||||
if let Some(operation_id) = &result.operation_id {
|
||||
let poll_spinner = if output_format == OutputFormat::Pretty {
|
||||
Some(ui::create_spinner("Processing memories..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (success, error_msg) = client.poll_operation(agent_id, operation_id, verbose)?;
|
||||
|
||||
if let Some(mut sp) = poll_spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
if success {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_success("Files retained successfully");
|
||||
println!(" Items processed: {}", result.items_count);
|
||||
} else {
|
||||
output::print_output(&result, output_format)?;
|
||||
}
|
||||
} else {
|
||||
let msg = error_msg.unwrap_or_else(|| "Unknown error".to_string());
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_error(&format!("Retain operation failed: {}", msg));
|
||||
}
|
||||
anyhow::bail!("Retain operation failed: {}", msg);
|
||||
}
|
||||
} else {
|
||||
// No operation ID returned, shouldn't happen with async=true
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_success("Files retained successfully");
|
||||
println!(" Items processed: {}", result.items_count);
|
||||
} else {
|
||||
output::print_output(&result, output_format)?;
|
||||
}
|
||||
}
|
||||
output::print_output(&result, output_format)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -473,84 +428,3 @@ pub fn clear(
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_is_text_file_supported_extensions() {
|
||||
let supported = [
|
||||
"file.txt", "file.md", "file.json", "file.yaml", "file.yml",
|
||||
"file.toml", "file.xml", "file.csv", "file.log", "file.rst", "file.adoc",
|
||||
];
|
||||
for filename in supported {
|
||||
assert!(
|
||||
is_text_file(Path::new(filename)),
|
||||
"{} should be recognized as a text file",
|
||||
filename
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_text_file_case_insensitive() {
|
||||
assert!(is_text_file(Path::new("file.JSON")));
|
||||
assert!(is_text_file(Path::new("file.TXT")));
|
||||
assert!(is_text_file(Path::new("file.Md")));
|
||||
assert!(is_text_file(Path::new("file.YAML")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_text_file_unsupported_extensions() {
|
||||
let unsupported = [
|
||||
"file.pdf", "file.doc", "file.docx", "file.png", "file.jpg",
|
||||
"file.exe", "file.bin", "file.zip", "file.tar", "file.gz",
|
||||
];
|
||||
for filename in unsupported {
|
||||
assert!(
|
||||
!is_text_file(Path::new(filename)),
|
||||
"{} should NOT be recognized as a text file",
|
||||
filename
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_text_file_no_extension() {
|
||||
assert!(!is_text_file(Path::new("README")));
|
||||
assert!(!is_text_file(Path::new("Makefile")));
|
||||
assert!(!is_text_file(Path::new(".gitignore")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_text_file_with_path() {
|
||||
assert!(is_text_file(Path::new("/some/path/to/file.json")));
|
||||
assert!(is_text_file(Path::new("../relative/path/file.md")));
|
||||
assert!(!is_text_file(Path::new("/path/to/image.png")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_budget_valid_values() {
|
||||
assert!(matches!(parse_budget("low"), Budget::Low));
|
||||
assert!(matches!(parse_budget("mid"), Budget::Mid));
|
||||
assert!(matches!(parse_budget("high"), Budget::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_budget_case_insensitive() {
|
||||
assert!(matches!(parse_budget("LOW"), Budget::Low));
|
||||
assert!(matches!(parse_budget("MID"), Budget::Mid));
|
||||
assert!(matches!(parse_budget("HIGH"), Budget::High));
|
||||
assert!(matches!(parse_budget("Low"), Budget::Low));
|
||||
assert!(matches!(parse_budget("High"), Budget::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_budget_defaults_to_mid() {
|
||||
assert!(matches!(parse_budget("invalid"), Budget::Mid));
|
||||
assert!(matches!(parse_budget(""), Budget::Mid));
|
||||
assert!(matches!(parse_budget("unknown"), Budget::Mid));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ const DEFAULT_API_URL: &str = "http://localhost:8888";
|
||||
const CONFIG_FILE_NAME: &str = "config";
|
||||
const CONFIG_DIR_NAME: &str = ".hindsight";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub api_url: String,
|
||||
pub api_key: Option<String>,
|
||||
@@ -175,156 +174,3 @@ pub fn generate_doc_id() -> String {
|
||||
let now = chrono::Local::now();
|
||||
format!("cli_put_{}", now.format("%Y%m%d_%H%M%S"))
|
||||
}
|
||||
|
||||
/// Parse a simple TOML-like config line and extract value.
|
||||
/// Handles both quoted and unquoted values.
|
||||
pub fn parse_config_value(line: &str, key: &str) -> Option<String> {
|
||||
let line = line.trim();
|
||||
if !line.starts_with(key) {
|
||||
return None;
|
||||
}
|
||||
line.split('=').nth(1).map(|value| {
|
||||
value.trim().trim_matches('"').trim_matches('\'').to_string()
|
||||
}).filter(|v| !v.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_source_display() {
|
||||
assert_eq!(format!("{}", ConfigSource::LocalFile), "config file");
|
||||
assert_eq!(format!("{}", ConfigSource::Environment), "environment variable");
|
||||
assert_eq!(format!("{}", ConfigSource::Default), "default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_and_create_valid_http() {
|
||||
let config = Config::validate_and_create(
|
||||
"http://localhost:8888".to_string(),
|
||||
None,
|
||||
ConfigSource::Default,
|
||||
);
|
||||
assert!(config.is_ok());
|
||||
let config = config.unwrap();
|
||||
assert_eq!(config.api_url, "http://localhost:8888");
|
||||
assert_eq!(config.source, ConfigSource::Default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_and_create_valid_https() {
|
||||
let config = Config::validate_and_create(
|
||||
"https://api.example.com".to_string(),
|
||||
Some("secret-key".to_string()),
|
||||
ConfigSource::Environment,
|
||||
);
|
||||
assert!(config.is_ok());
|
||||
let config = config.unwrap();
|
||||
assert_eq!(config.api_url, "https://api.example.com");
|
||||
assert_eq!(config.api_key, Some("secret-key".to_string()));
|
||||
assert_eq!(config.source, ConfigSource::Environment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_and_create_invalid_url() {
|
||||
let config = Config::validate_and_create(
|
||||
"localhost:8888".to_string(),
|
||||
None,
|
||||
ConfigSource::Default,
|
||||
);
|
||||
assert!(config.is_err());
|
||||
let err = config.unwrap_err().to_string();
|
||||
assert!(err.contains("Invalid API URL"));
|
||||
assert!(err.contains("Must start with http:// or https://"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_and_create_ftp_url() {
|
||||
let config = Config::validate_and_create(
|
||||
"ftp://example.com".to_string(),
|
||||
None,
|
||||
ConfigSource::Default,
|
||||
);
|
||||
assert!(config.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_doc_id_format() {
|
||||
let doc_id = generate_doc_id();
|
||||
assert!(doc_id.starts_with("cli_put_"));
|
||||
// Should be cli_put_YYYYMMDD_HHMMSS format
|
||||
assert!(doc_id.len() > 20); // cli_put_ (8) + date (8) + _ (1) + time (6) = 23
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_doc_id_uniqueness() {
|
||||
let id1 = generate_doc_id();
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
let id2 = generate_doc_id();
|
||||
// IDs generated at different times should be different
|
||||
assert_ne!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_config_value_quoted() {
|
||||
assert_eq!(
|
||||
parse_config_value(r#"api_url = "http://localhost:8888""#, "api_url"),
|
||||
Some("http://localhost:8888".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_config_value_single_quoted() {
|
||||
assert_eq!(
|
||||
parse_config_value("api_url = 'http://localhost:8888'", "api_url"),
|
||||
Some("http://localhost:8888".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_config_value_unquoted() {
|
||||
assert_eq!(
|
||||
parse_config_value("api_url = http://localhost:8888", "api_url"),
|
||||
Some("http://localhost:8888".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_config_value_with_spaces() {
|
||||
assert_eq!(
|
||||
parse_config_value(" api_url = \"http://localhost:8888\" ", "api_url"),
|
||||
Some("http://localhost:8888".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_config_value_wrong_key() {
|
||||
assert_eq!(
|
||||
parse_config_value("api_key = secret", "api_url"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_config_value_empty() {
|
||||
assert_eq!(
|
||||
parse_config_value("api_url = ", "api_url"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
parse_config_value("api_url = \"\"", "api_url"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_api_url_accessor() {
|
||||
let config = Config {
|
||||
api_url: "http://test:8080".to_string(),
|
||||
api_key: None,
|
||||
source: ConfigSource::Default,
|
||||
};
|
||||
assert_eq!(config.api_url(), "http://test:8080");
|
||||
}
|
||||
}
|
||||
|
||||
+2
-142
@@ -8,35 +8,13 @@ pub enum OutputFormat {
|
||||
Yaml,
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
/// Parse output format from string
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"json" => Some(OutputFormat::Json),
|
||||
"yaml" | "yml" => Some(OutputFormat::Yaml),
|
||||
"pretty" | "text" => Some(OutputFormat::Pretty),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format data as JSON string
|
||||
pub fn to_json<T: Serialize>(data: &T) -> Result<String> {
|
||||
Ok(serde_json::to_string_pretty(data)?)
|
||||
}
|
||||
|
||||
/// Format data as YAML string
|
||||
pub fn to_yaml<T: Serialize>(data: &T) -> Result<String> {
|
||||
Ok(serde_yaml::to_string(data)?)
|
||||
}
|
||||
|
||||
pub fn print_output<T: Serialize>(data: &T, format: OutputFormat) -> Result<()> {
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
println!("{}", to_json(data)?);
|
||||
println!("{}", serde_json::to_string_pretty(data)?);
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
println!("{}", to_yaml(data)?);
|
||||
println!("{}", serde_yaml::to_string(data)?);
|
||||
}
|
||||
OutputFormat::Pretty => {
|
||||
// This should not be called - pretty printing is handled in ui.rs
|
||||
@@ -45,121 +23,3 @@ pub fn print_output<T: Serialize>(data: &T, format: OutputFormat) -> Result<()>
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
struct TestData {
|
||||
name: String,
|
||||
count: i32,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_format_from_str_json() {
|
||||
assert_eq!(OutputFormat::from_str("json"), Some(OutputFormat::Json));
|
||||
assert_eq!(OutputFormat::from_str("JSON"), Some(OutputFormat::Json));
|
||||
assert_eq!(OutputFormat::from_str("Json"), Some(OutputFormat::Json));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_format_from_str_yaml() {
|
||||
assert_eq!(OutputFormat::from_str("yaml"), Some(OutputFormat::Yaml));
|
||||
assert_eq!(OutputFormat::from_str("YAML"), Some(OutputFormat::Yaml));
|
||||
assert_eq!(OutputFormat::from_str("yml"), Some(OutputFormat::Yaml));
|
||||
assert_eq!(OutputFormat::from_str("YML"), Some(OutputFormat::Yaml));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_format_from_str_pretty() {
|
||||
assert_eq!(OutputFormat::from_str("pretty"), Some(OutputFormat::Pretty));
|
||||
assert_eq!(OutputFormat::from_str("PRETTY"), Some(OutputFormat::Pretty));
|
||||
assert_eq!(OutputFormat::from_str("text"), Some(OutputFormat::Pretty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_format_from_str_invalid() {
|
||||
assert_eq!(OutputFormat::from_str("xml"), None);
|
||||
assert_eq!(OutputFormat::from_str("csv"), None);
|
||||
assert_eq!(OutputFormat::from_str(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_json() {
|
||||
let data = TestData {
|
||||
name: "test".to_string(),
|
||||
count: 42,
|
||||
active: true,
|
||||
};
|
||||
let json = to_json(&data).unwrap();
|
||||
assert!(json.contains("\"name\": \"test\""));
|
||||
assert!(json.contains("\"count\": 42"));
|
||||
assert!(json.contains("\"active\": true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_yaml() {
|
||||
let data = TestData {
|
||||
name: "test".to_string(),
|
||||
count: 42,
|
||||
active: true,
|
||||
};
|
||||
let yaml = to_yaml(&data).unwrap();
|
||||
assert!(yaml.contains("name: test"));
|
||||
assert!(yaml.contains("count: 42"));
|
||||
assert!(yaml.contains("active: true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_json_array() {
|
||||
let data = vec![
|
||||
TestData { name: "a".to_string(), count: 1, active: true },
|
||||
TestData { name: "b".to_string(), count: 2, active: false },
|
||||
];
|
||||
let json = to_json(&data).unwrap();
|
||||
assert!(json.contains("\"name\": \"a\""));
|
||||
assert!(json.contains("\"name\": \"b\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_yaml_array() {
|
||||
let data = vec![
|
||||
TestData { name: "a".to_string(), count: 1, active: true },
|
||||
TestData { name: "b".to_string(), count: 2, active: false },
|
||||
];
|
||||
let yaml = to_yaml(&data).unwrap();
|
||||
assert!(yaml.contains("name: a"));
|
||||
assert!(yaml.contains("name: b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_format_equality() {
|
||||
assert_eq!(OutputFormat::Json, OutputFormat::Json);
|
||||
assert_ne!(OutputFormat::Json, OutputFormat::Yaml);
|
||||
assert_ne!(OutputFormat::Yaml, OutputFormat::Pretty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_format_clone() {
|
||||
let format = OutputFormat::Json;
|
||||
let cloned = format.clone();
|
||||
assert_eq!(format, cloned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_json_special_chars() {
|
||||
let data = TestData {
|
||||
name: "test\"with\\special\nchars".to_string(),
|
||||
count: 0,
|
||||
active: false,
|
||||
};
|
||||
let json = to_json(&data).unwrap();
|
||||
// JSON should properly escape special characters
|
||||
assert!(json.contains("\\\""));
|
||||
assert!(json.contains("\\\\"));
|
||||
assert!(json.contains("\\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,9 @@ hindsight_client_api/models/list_documents_response.py
|
||||
hindsight_client_api/models/list_memory_units_response.py
|
||||
hindsight_client_api/models/list_tags_response.py
|
||||
hindsight_client_api/models/memory_item.py
|
||||
hindsight_client_api/models/mental_model_freshness_response.py
|
||||
hindsight_client_api/models/mental_model_list_response.py
|
||||
hindsight_client_api/models/mental_model_observation_response.py
|
||||
hindsight_client_api/models/mental_model_response.py
|
||||
hindsight_client_api/models/observation_evidence_response.py
|
||||
hindsight_client_api/models/observation_input.py
|
||||
hindsight_client_api/models/operation_response.py
|
||||
hindsight_client_api/models/operation_status_response.py
|
||||
hindsight_client_api/models/operations_list_response.py
|
||||
@@ -73,7 +70,6 @@ hindsight_client_api/models/tag_item.py
|
||||
hindsight_client_api/models/token_usage.py
|
||||
hindsight_client_api/models/tool_calls_include_options.py
|
||||
hindsight_client_api/models/update_disposition_request.py
|
||||
hindsight_client_api/models/update_mental_model_request.py
|
||||
hindsight_client_api/models/validation_error.py
|
||||
hindsight_client_api/models/validation_error_loc_inner.py
|
||||
hindsight_client_api/rest.py
|
||||
|
||||
@@ -115,7 +115,6 @@ class Hindsight:
|
||||
document_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
entities: Optional[List[Dict[str, str]]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
) -> RetainResponse:
|
||||
"""
|
||||
Store a single memory (simplified interface).
|
||||
@@ -128,14 +127,13 @@ class Hindsight:
|
||||
document_id: Optional document ID for grouping
|
||||
metadata: Optional user-defined metadata
|
||||
entities: Optional list of entities [{"text": "...", "type": "..."}]
|
||||
tags: Optional list of tags for this memory
|
||||
|
||||
Returns:
|
||||
RetainResponse with success status
|
||||
"""
|
||||
return self.retain_batch(
|
||||
bank_id=bank_id,
|
||||
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata, "entities": entities, "tags": tags}],
|
||||
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata, "entities": entities}],
|
||||
document_id=document_id,
|
||||
)
|
||||
|
||||
@@ -145,17 +143,15 @@ class Hindsight:
|
||||
items: List[Dict[str, Any]],
|
||||
document_id: Optional[str] = None,
|
||||
retain_async: bool = False,
|
||||
document_tags: Optional[List[str]] = None,
|
||||
) -> RetainResponse:
|
||||
"""
|
||||
Store multiple memories in batch.
|
||||
|
||||
Args:
|
||||
bank_id: The memory bank ID
|
||||
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id', 'entities', 'tags'
|
||||
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id', 'entities'
|
||||
document_id: Optional document ID for grouping memories (applied to items that don't have their own)
|
||||
retain_async: If True, process asynchronously in background (default: False)
|
||||
document_tags: Optional list of tags to apply to all memories in this batch
|
||||
|
||||
Returns:
|
||||
RetainResponse with success status and item count
|
||||
@@ -179,14 +175,12 @@ class Hindsight:
|
||||
# Use item's document_id if provided, otherwise fall back to batch-level document_id
|
||||
document_id=item.get("document_id") or document_id,
|
||||
entities=entities,
|
||||
tags=item.get("tags"),
|
||||
)
|
||||
)
|
||||
|
||||
request_obj = retain_request.RetainRequest(
|
||||
items=memory_items,
|
||||
async_=retain_async,
|
||||
document_tags=document_tags,
|
||||
)
|
||||
|
||||
return _run_async(self._memory_api.retain_memories(bank_id, request_obj))
|
||||
@@ -204,8 +198,6 @@ class Hindsight:
|
||||
max_entity_tokens: int = 500,
|
||||
include_chunks: bool = False,
|
||||
max_chunk_tokens: int = 8192,
|
||||
tags: Optional[List[str]] = None,
|
||||
tags_match: str = "any",
|
||||
) -> RecallResponse:
|
||||
"""
|
||||
Recall memories using semantic similarity.
|
||||
@@ -222,9 +214,6 @@ class Hindsight:
|
||||
max_entity_tokens: Maximum tokens for entity observations (default: 500)
|
||||
include_chunks: Include raw text chunks in results (default: False)
|
||||
max_chunk_tokens: Maximum tokens for chunks (default: 8192)
|
||||
tags: Optional list of tags to filter memories by
|
||||
tags_match: How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged),
|
||||
'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any'
|
||||
|
||||
Returns:
|
||||
RecallResponse with results, optional entities, optional chunks, and optional trace
|
||||
@@ -244,8 +233,6 @@ class Hindsight:
|
||||
trace=trace,
|
||||
query_timestamp=query_timestamp,
|
||||
include=include_opts,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
)
|
||||
|
||||
return _run_async(self._memory_api.recall_memories(bank_id, request_obj))
|
||||
@@ -258,8 +245,6 @@ class Hindsight:
|
||||
context: Optional[str] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
response_schema: Optional[Dict[str, Any]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
tags_match: str = "any",
|
||||
) -> ReflectResponse:
|
||||
"""
|
||||
Generate a contextual answer based on bank identity and memories.
|
||||
@@ -273,9 +258,6 @@ class Hindsight:
|
||||
response_schema: Optional JSON Schema for structured output. When provided,
|
||||
the response will include a 'structured_output' field with the LLM
|
||||
response parsed according to this schema.
|
||||
tags: Optional list of tags to filter memories by
|
||||
tags_match: How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged),
|
||||
'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any'
|
||||
|
||||
Returns:
|
||||
ReflectResponse with answer text, optionally facts used, and optionally
|
||||
@@ -287,8 +269,6 @@ class Hindsight:
|
||||
context=context,
|
||||
max_tokens=max_tokens,
|
||||
response_schema=response_schema,
|
||||
tags=tags,
|
||||
tags_match=tags_match,
|
||||
)
|
||||
|
||||
return _run_async(self._memory_api.reflect(bank_id, request_obj))
|
||||
|
||||
@@ -70,12 +70,9 @@ from hindsight_client_api.models.list_documents_response import ListDocumentsRes
|
||||
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
|
||||
from hindsight_client_api.models.list_tags_response import ListTagsResponse
|
||||
from hindsight_client_api.models.memory_item import MemoryItem
|
||||
from hindsight_client_api.models.mental_model_freshness_response import MentalModelFreshnessResponse
|
||||
from hindsight_client_api.models.mental_model_list_response import MentalModelListResponse
|
||||
from hindsight_client_api.models.mental_model_observation_response import MentalModelObservationResponse
|
||||
from hindsight_client_api.models.mental_model_response import MentalModelResponse
|
||||
from hindsight_client_api.models.observation_evidence_response import ObservationEvidenceResponse
|
||||
from hindsight_client_api.models.observation_input import ObservationInput
|
||||
from hindsight_client_api.models.operation_response import OperationResponse
|
||||
from hindsight_client_api.models.operation_status_response import OperationStatusResponse
|
||||
from hindsight_client_api.models.operations_list_response import OperationsListResponse
|
||||
@@ -98,6 +95,5 @@ from hindsight_client_api.models.tag_item import TagItem
|
||||
from hindsight_client_api.models.token_usage import TokenUsage
|
||||
from hindsight_client_api.models.tool_calls_include_options import ToolCallsIncludeOptions
|
||||
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
|
||||
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
|
||||
from hindsight_client_api.models.validation_error import ValidationError
|
||||
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -47,12 +47,9 @@ from hindsight_client_api.models.list_documents_response import ListDocumentsRes
|
||||
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
|
||||
from hindsight_client_api.models.list_tags_response import ListTagsResponse
|
||||
from hindsight_client_api.models.memory_item import MemoryItem
|
||||
from hindsight_client_api.models.mental_model_freshness_response import MentalModelFreshnessResponse
|
||||
from hindsight_client_api.models.mental_model_list_response import MentalModelListResponse
|
||||
from hindsight_client_api.models.mental_model_observation_response import MentalModelObservationResponse
|
||||
from hindsight_client_api.models.mental_model_response import MentalModelResponse
|
||||
from hindsight_client_api.models.observation_evidence_response import ObservationEvidenceResponse
|
||||
from hindsight_client_api.models.observation_input import ObservationInput
|
||||
from hindsight_client_api.models.operation_response import OperationResponse
|
||||
from hindsight_client_api.models.operation_status_response import OperationStatusResponse
|
||||
from hindsight_client_api.models.operations_list_response import OperationsListResponse
|
||||
@@ -75,6 +72,5 @@ from hindsight_client_api.models.tag_item import TagItem
|
||||
from hindsight_client_api.models.token_usage import TokenUsage
|
||||
from hindsight_client_api.models.tool_calls_include_options import ToolCallsIncludeOptions
|
||||
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
|
||||
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
|
||||
from hindsight_client_api.models.validation_error import ValidationError
|
||||
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
|
||||
|
||||
+2
-19
@@ -19,20 +19,17 @@ import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from hindsight_client_api.models.observation_input import ObservationInput
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class CreateMentalModelRequest(BaseModel):
|
||||
"""
|
||||
Request model for creating a mental model.
|
||||
Request model for creating a pinned mental model.
|
||||
""" # noqa: E501
|
||||
name: StrictStr = Field(description="Human-readable name for the mental model")
|
||||
description: StrictStr = Field(description="One-liner description for quick scanning")
|
||||
subtype: Optional[StrictStr] = Field(default='pinned', description="Type of mental model: 'pinned' (observations LLM-generated) or 'directive' (observations user-provided)")
|
||||
observations: Optional[List[ObservationInput]] = None
|
||||
tags: Optional[List[StrictStr]] = Field(default=None, description="Tags for scoped visibility")
|
||||
__properties: ClassVar[List[str]] = ["name", "description", "subtype", "observations", "tags"]
|
||||
__properties: ClassVar[List[str]] = ["name", "description", "tags"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -73,18 +70,6 @@ class CreateMentalModelRequest(BaseModel):
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# override the default output from pydantic by calling `to_dict()` of each item in observations (list)
|
||||
_items = []
|
||||
if self.observations:
|
||||
for _item_observations in self.observations:
|
||||
if _item_observations:
|
||||
_items.append(_item_observations.to_dict())
|
||||
_dict['observations'] = _items
|
||||
# set to None if observations (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.observations is None and "observations" in self.model_fields_set:
|
||||
_dict['observations'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
@@ -99,8 +84,6 @@ class CreateMentalModelRequest(BaseModel):
|
||||
_obj = cls.model_validate({
|
||||
"name": obj.get("name"),
|
||||
"description": obj.get("description"),
|
||||
"subtype": obj.get("subtype") if obj.get("subtype") is not None else 'pinned',
|
||||
"observations": [ObservationInput.from_dict(_item) for _item in obj["observations"]] if obj.get("observations") is not None else None,
|
||||
"tags": obj.get("tags")
|
||||
})
|
||||
return _obj
|
||||
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.1.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, Field, StrictBool, StrictInt, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class MentalModelFreshnessResponse(BaseModel):
|
||||
"""
|
||||
Freshness information for a mental model.
|
||||
""" # noqa: E501
|
||||
is_up_to_date: StrictBool = Field(description="Whether the model has been refreshed since the last memory was added")
|
||||
last_refresh_at: Optional[StrictStr]
|
||||
memories_since_refresh: StrictInt = Field(description="Number of memories added since last refresh")
|
||||
reasons: Optional[List[StrictStr]] = Field(default=None, description="Reasons why the model needs refresh (empty if up to date). Possible values: never_refreshed, new_memories, mission_changed, disposition_changed, directives_changed")
|
||||
__properties: ClassVar[List[str]] = ["is_up_to_date", "last_refresh_at", "memories_since_refresh", "reasons"]
|
||||
|
||||
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 MentalModelFreshnessResponse 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,
|
||||
)
|
||||
# set to None if last_refresh_at (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.last_refresh_at is None and "last_refresh_at" in self.model_fields_set:
|
||||
_dict['last_refresh_at'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of MentalModelFreshnessResponse from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"is_up_to_date": obj.get("is_up_to_date"),
|
||||
"last_refresh_at": obj.get("last_refresh_at"),
|
||||
"memories_since_refresh": obj.get("memories_since_refresh"),
|
||||
"reasons": obj.get("reasons")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
+8
-24
@@ -17,24 +17,19 @@ import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from hindsight_client_api.models.observation_evidence_response import ObservationEvidenceResponse
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class MentalModelObservationResponse(BaseModel):
|
||||
"""
|
||||
An observation within a mental model with its supporting evidence.
|
||||
An observation within a mental model with its supporting memories.
|
||||
""" # noqa: E501
|
||||
title: StrictStr = Field(description="Short summary title for the observation")
|
||||
content: StrictStr = Field(description="The observation content - detailed explanation")
|
||||
evidence: Optional[List[ObservationEvidenceResponse]] = Field(default=None, description="Supporting evidence with quotes")
|
||||
created_at: StrictStr = Field(description="When this observation was first created (ISO format)")
|
||||
trend: StrictStr = Field(description="Computed trend: stable, strengthening, weakening, new, stale")
|
||||
evidence_count: StrictInt = Field(description="Number of evidence items supporting this observation")
|
||||
evidence_span: Dict[str, Any] = Field(description="Time span of evidence: {from: iso_date, to: iso_date}")
|
||||
__properties: ClassVar[List[str]] = ["title", "content", "evidence", "created_at", "trend", "evidence_count", "evidence_span"]
|
||||
title: StrictStr = Field(description="Observation header (empty for intro)")
|
||||
text: StrictStr = Field(description="Observation content")
|
||||
based_on: Optional[List[StrictStr]] = Field(default=None, description="Memory IDs supporting this observation")
|
||||
__properties: ClassVar[List[str]] = ["title", "text", "based_on"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -75,13 +70,6 @@ class MentalModelObservationResponse(BaseModel):
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# override the default output from pydantic by calling `to_dict()` of each item in evidence (list)
|
||||
_items = []
|
||||
if self.evidence:
|
||||
for _item_evidence in self.evidence:
|
||||
if _item_evidence:
|
||||
_items.append(_item_evidence.to_dict())
|
||||
_dict['evidence'] = _items
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
@@ -95,12 +83,8 @@ class MentalModelObservationResponse(BaseModel):
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"title": obj.get("title"),
|
||||
"content": obj.get("content"),
|
||||
"evidence": [ObservationEvidenceResponse.from_dict(_item) for _item in obj["evidence"]] if obj.get("evidence") is not None else None,
|
||||
"created_at": obj.get("created_at"),
|
||||
"trend": obj.get("trend"),
|
||||
"evidence_count": obj.get("evidence_count"),
|
||||
"evidence_span": obj.get("evidence_span")
|
||||
"text": obj.get("text"),
|
||||
"based_on": obj.get("based_on")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -17,9 +17,8 @@ import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from hindsight_client_api.models.mental_model_freshness_response import MentalModelFreshnessResponse
|
||||
from hindsight_client_api.models.mental_model_observation_response import MentalModelObservationResponse
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
@@ -34,15 +33,12 @@ class MentalModelResponse(BaseModel):
|
||||
name: StrictStr
|
||||
description: StrictStr
|
||||
observations: Optional[List[MentalModelObservationResponse]] = Field(default=None, description="Structured observations with per-observation fact attribution")
|
||||
version: Optional[StrictInt] = Field(default=0, description="Version number of the mental model observations")
|
||||
entity_id: Optional[StrictStr] = None
|
||||
links: Optional[List[StrictStr]] = None
|
||||
tags: Optional[List[StrictStr]] = None
|
||||
last_updated: Optional[StrictStr] = None
|
||||
last_refresh_at: Optional[StrictStr] = None
|
||||
freshness: Optional[MentalModelFreshnessResponse] = None
|
||||
created_at: StrictStr
|
||||
__properties: ClassVar[List[str]] = ["id", "bank_id", "subtype", "name", "description", "observations", "version", "entity_id", "links", "tags", "last_updated", "last_refresh_at", "freshness", "created_at"]
|
||||
__properties: ClassVar[List[str]] = ["id", "bank_id", "subtype", "name", "description", "observations", "entity_id", "links", "tags", "last_updated", "created_at"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -90,9 +86,6 @@ class MentalModelResponse(BaseModel):
|
||||
if _item_observations:
|
||||
_items.append(_item_observations.to_dict())
|
||||
_dict['observations'] = _items
|
||||
# override the default output from pydantic by calling `to_dict()` of freshness
|
||||
if self.freshness:
|
||||
_dict['freshness'] = self.freshness.to_dict()
|
||||
# set to None if entity_id (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.entity_id is None and "entity_id" in self.model_fields_set:
|
||||
@@ -103,16 +96,6 @@ class MentalModelResponse(BaseModel):
|
||||
if self.last_updated is None and "last_updated" in self.model_fields_set:
|
||||
_dict['last_updated'] = None
|
||||
|
||||
# set to None if last_refresh_at (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.last_refresh_at is None and "last_refresh_at" in self.model_fields_set:
|
||||
_dict['last_refresh_at'] = None
|
||||
|
||||
# set to None if freshness (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.freshness is None and "freshness" in self.model_fields_set:
|
||||
_dict['freshness'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
@@ -131,13 +114,10 @@ class MentalModelResponse(BaseModel):
|
||||
"name": obj.get("name"),
|
||||
"description": obj.get("description"),
|
||||
"observations": [MentalModelObservationResponse.from_dict(_item) for _item in obj["observations"]] if obj.get("observations") is not None else None,
|
||||
"version": obj.get("version") if obj.get("version") is not None else 0,
|
||||
"entity_id": obj.get("entity_id"),
|
||||
"links": obj.get("links"),
|
||||
"tags": obj.get("tags"),
|
||||
"last_updated": obj.get("last_updated"),
|
||||
"last_refresh_at": obj.get("last_refresh_at"),
|
||||
"freshness": MentalModelFreshnessResponse.from_dict(obj["freshness"]) if obj.get("freshness") is not None else None,
|
||||
"created_at": obj.get("created_at")
|
||||
})
|
||||
return _obj
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.1.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, Field, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class ObservationEvidenceResponse(BaseModel):
|
||||
"""
|
||||
A single piece of evidence supporting an observation.
|
||||
""" # noqa: E501
|
||||
memory_id: StrictStr = Field(description="ID of the memory unit this evidence comes from")
|
||||
quote: StrictStr = Field(description="Exact quote from the memory supporting the observation")
|
||||
relevance: StrictStr = Field(description="Brief explanation of how this quote supports the observation")
|
||||
timestamp: StrictStr = Field(description="When the source memory was created (ISO format)")
|
||||
__properties: ClassVar[List[str]] = ["memory_id", "quote", "relevance", "timestamp"]
|
||||
|
||||
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 ObservationEvidenceResponse 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 ObservationEvidenceResponse from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"memory_id": obj.get("memory_id"),
|
||||
"quote": obj.get("quote"),
|
||||
"relevance": obj.get("relevance"),
|
||||
"timestamp": obj.get("timestamp")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.1.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, Field, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class ObservationInput(BaseModel):
|
||||
"""
|
||||
Input model for a single observation.
|
||||
""" # noqa: E501
|
||||
title: StrictStr = Field(description="Short title/header for the observation")
|
||||
content: StrictStr = Field(description="Content of the observation")
|
||||
__properties: ClassVar[List[str]] = ["title", "content"]
|
||||
|
||||
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 ObservationInput 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 ObservationInput from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"title": obj.get("title"),
|
||||
"content": obj.get("content")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -29,9 +29,10 @@ class ReflectMentalModel(BaseModel):
|
||||
id: StrictStr = Field(description="Mental model ID")
|
||||
name: StrictStr = Field(description="Mental model name")
|
||||
type: StrictStr = Field(description="Mental model type: entity, concept, event")
|
||||
subtype: StrictStr = Field(description="Mental model subtype: structural, emergent, learned, directive")
|
||||
observations: Optional[List[StrictStr]] = None
|
||||
__properties: ClassVar[List[str]] = ["id", "name", "type", "subtype", "observations"]
|
||||
subtype: StrictStr = Field(description="Mental model subtype: structural, emergent, learned")
|
||||
description: StrictStr = Field(description="Brief description")
|
||||
summary: Optional[StrictStr] = None
|
||||
__properties: ClassVar[List[str]] = ["id", "name", "type", "subtype", "description", "summary"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -72,10 +73,10 @@ class ReflectMentalModel(BaseModel):
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# set to None if observations (nullable) is None
|
||||
# set to None if summary (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.observations is None and "observations" in self.model_fields_set:
|
||||
_dict['observations'] = None
|
||||
if self.summary is None and "summary" in self.model_fields_set:
|
||||
_dict['summary'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@@ -93,7 +94,8 @@ class ReflectMentalModel(BaseModel):
|
||||
"name": obj.get("name"),
|
||||
"type": obj.get("type"),
|
||||
"subtype": obj.get("subtype"),
|
||||
"observations": obj.get("observations")
|
||||
"description": obj.get("description"),
|
||||
"summary": obj.get("summary")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import json
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from hindsight_client_api.models.reflect_llm_call import ReflectLLMCall
|
||||
from hindsight_client_api.models.reflect_mental_model import ReflectMentalModel
|
||||
from hindsight_client_api.models.reflect_tool_call import ReflectToolCall
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
@@ -31,8 +30,7 @@ class ReflectTrace(BaseModel):
|
||||
""" # noqa: E501
|
||||
tool_calls: Optional[List[ReflectToolCall]] = Field(default=None, description="Tool calls made during reflection")
|
||||
llm_calls: Optional[List[ReflectLLMCall]] = Field(default=None, description="LLM calls made during reflection")
|
||||
mental_models: Optional[List[ReflectMentalModel]] = Field(default=None, description="Mental models used during reflection (includes directives with subtype='directive')")
|
||||
__properties: ClassVar[List[str]] = ["tool_calls", "llm_calls", "mental_models"]
|
||||
__properties: ClassVar[List[str]] = ["tool_calls", "llm_calls"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -87,13 +85,6 @@ class ReflectTrace(BaseModel):
|
||||
if _item_llm_calls:
|
||||
_items.append(_item_llm_calls.to_dict())
|
||||
_dict['llm_calls'] = _items
|
||||
# override the default output from pydantic by calling `to_dict()` of each item in mental_models (list)
|
||||
_items = []
|
||||
if self.mental_models:
|
||||
for _item_mental_models in self.mental_models:
|
||||
if _item_mental_models:
|
||||
_items.append(_item_mental_models.to_dict())
|
||||
_dict['mental_models'] = _items
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
@@ -107,8 +98,7 @@ class ReflectTrace(BaseModel):
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"tool_calls": [ReflectToolCall.from_dict(_item) for _item in obj["tool_calls"]] if obj.get("tool_calls") is not None else None,
|
||||
"llm_calls": [ReflectLLMCall.from_dict(_item) for _item in obj["llm_calls"]] if obj.get("llm_calls") is not None else None,
|
||||
"mental_models": [ReflectMentalModel.from_dict(_item) for _item in obj["mental_models"]] if obj.get("mental_models") is not None else None
|
||||
"llm_calls": [ReflectLLMCall.from_dict(_item) for _item in obj["llm_calls"]] if obj.get("llm_calls") is not None else None
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.1.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, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class UpdateMentalModelRequest(BaseModel):
|
||||
"""
|
||||
Request model for updating a mental model.
|
||||
""" # noqa: E501
|
||||
name: Optional[StrictStr] = None
|
||||
description: Optional[StrictStr] = None
|
||||
__properties: ClassVar[List[str]] = ["name", "description"]
|
||||
|
||||
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 UpdateMentalModelRequest 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,
|
||||
)
|
||||
# set to None if name (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.name is None and "name" in self.model_fields_set:
|
||||
_dict['name'] = None
|
||||
|
||||
# set to None if description (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.description is None and "description" in self.model_fields_set:
|
||||
_dict['description'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of UpdateMentalModelRequest from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"name": obj.get("name"),
|
||||
"description": obj.get("description")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ import type {
|
||||
DeleteMentalModelData,
|
||||
DeleteMentalModelErrors,
|
||||
DeleteMentalModelResponses,
|
||||
GenerateMentalModelData,
|
||||
GenerateMentalModelErrors,
|
||||
GenerateMentalModelResponses,
|
||||
GetAgentStatsData,
|
||||
GetAgentStatsErrors,
|
||||
GetAgentStatsResponses,
|
||||
@@ -51,9 +54,6 @@ import type {
|
||||
GetMentalModelData,
|
||||
GetMentalModelErrors,
|
||||
GetMentalModelResponses,
|
||||
GetMentalModelVersionData,
|
||||
GetMentalModelVersionErrors,
|
||||
GetMentalModelVersionResponses,
|
||||
GetOperationStatusData,
|
||||
GetOperationStatusErrors,
|
||||
GetOperationStatusResponses,
|
||||
@@ -74,9 +74,6 @@ import type {
|
||||
ListMentalModelsData,
|
||||
ListMentalModelsErrors,
|
||||
ListMentalModelsResponses,
|
||||
ListMentalModelVersionsData,
|
||||
ListMentalModelVersionsErrors,
|
||||
ListMentalModelVersionsResponses,
|
||||
ListOperationsData,
|
||||
ListOperationsErrors,
|
||||
ListOperationsResponses,
|
||||
@@ -91,9 +88,6 @@ import type {
|
||||
ReflectData,
|
||||
ReflectErrors,
|
||||
ReflectResponses,
|
||||
RefreshMentalModelData,
|
||||
RefreshMentalModelErrors,
|
||||
RefreshMentalModelResponses,
|
||||
RefreshMentalModelsData,
|
||||
RefreshMentalModelsErrors,
|
||||
RefreshMentalModelsResponses,
|
||||
@@ -109,9 +103,6 @@ import type {
|
||||
UpdateBankDispositionResponses,
|
||||
UpdateBankErrors,
|
||||
UpdateBankResponses,
|
||||
UpdateMentalModelData,
|
||||
UpdateMentalModelErrors,
|
||||
UpdateMentalModelResponses,
|
||||
} from "./types.gen";
|
||||
|
||||
export type Options<
|
||||
@@ -352,9 +343,7 @@ export const listMentalModels = <ThrowOnError extends boolean = false>(
|
||||
/**
|
||||
* Create mental model
|
||||
*
|
||||
* Create a mental model. Supports two subtypes:
|
||||
* - 'pinned' (default): User-defined topic, observations are LLM-generated on refresh
|
||||
* - 'directive': User-defined hard rules, observations are provided at creation and never regenerated
|
||||
* Create a pinned mental model. Pinned models are user-defined and persist across refreshes.
|
||||
*/
|
||||
export const createMentalModel = <ThrowOnError extends boolean = false>(
|
||||
options: Options<CreateMentalModelData, ThrowOnError>,
|
||||
@@ -406,27 +395,6 @@ export const getMentalModel = <ThrowOnError extends boolean = false>(
|
||||
...options,
|
||||
});
|
||||
|
||||
/**
|
||||
* Update mental model
|
||||
*
|
||||
* Update a mental model's name and/or description. Useful for editing directives.
|
||||
*/
|
||||
export const updateMentalModel = <ThrowOnError extends boolean = false>(
|
||||
options: Options<UpdateMentalModelData, ThrowOnError>,
|
||||
) =>
|
||||
(options.client ?? client).patch<
|
||||
UpdateMentalModelResponses,
|
||||
UpdateMentalModelErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/v1/default/banks/{bank_id}/mental-models/{model_id}",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Refresh mental models (async)
|
||||
*
|
||||
@@ -449,53 +417,19 @@ export const refreshMentalModels = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
|
||||
/**
|
||||
* Refresh mental model content (async)
|
||||
* Generate mental model content (async)
|
||||
*
|
||||
* Submit a background job to refresh content for a specific mental model. This is useful for newly created learned models or to refresh content for any model.
|
||||
* Submit a background job to generate/refresh content for a specific mental model. This is useful for newly created learned models or to regenerate content for any model.
|
||||
*/
|
||||
export const refreshMentalModel = <ThrowOnError extends boolean = false>(
|
||||
options: Options<RefreshMentalModelData, ThrowOnError>,
|
||||
export const generateMentalModel = <ThrowOnError extends boolean = false>(
|
||||
options: Options<GenerateMentalModelData, ThrowOnError>,
|
||||
) =>
|
||||
(options.client ?? client).post<
|
||||
RefreshMentalModelResponses,
|
||||
RefreshMentalModelErrors,
|
||||
GenerateMentalModelResponses,
|
||||
GenerateMentalModelErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/v1/default/banks/{bank_id}/mental-models/{model_id}/refresh",
|
||||
...options,
|
||||
});
|
||||
|
||||
/**
|
||||
* List mental model version history
|
||||
*
|
||||
* List all saved versions of a mental model's observations, ordered by version descending.
|
||||
*/
|
||||
export const listMentalModelVersions = <ThrowOnError extends boolean = false>(
|
||||
options: Options<ListMentalModelVersionsData, ThrowOnError>,
|
||||
) =>
|
||||
(options.client ?? client).get<
|
||||
ListMentalModelVersionsResponses,
|
||||
ListMentalModelVersionsErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/v1/default/banks/{bank_id}/mental-models/{model_id}/versions",
|
||||
...options,
|
||||
});
|
||||
|
||||
/**
|
||||
* Get specific mental model version
|
||||
*
|
||||
* Get observations from a specific version of a mental model.
|
||||
*/
|
||||
export const getMentalModelVersion = <ThrowOnError extends boolean = false>(
|
||||
options: Options<GetMentalModelVersionData, ThrowOnError>,
|
||||
) =>
|
||||
(options.client ?? client).get<
|
||||
GetMentalModelVersionResponses,
|
||||
GetMentalModelVersionErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/v1/default/banks/{bank_id}/mental-models/{model_id}/versions/{version}",
|
||||
url: "/v1/default/banks/{bank_id}/mental-models/{model_id}/generate",
|
||||
...options,
|
||||
});
|
||||
|
||||
|
||||
@@ -314,7 +314,7 @@ export type CreateBankRequest = {
|
||||
/**
|
||||
* CreateMentalModelRequest
|
||||
*
|
||||
* Request model for creating a mental model.
|
||||
* Request model for creating a pinned mental model.
|
||||
*/
|
||||
export type CreateMentalModelRequest = {
|
||||
/**
|
||||
@@ -329,18 +329,6 @@ export type CreateMentalModelRequest = {
|
||||
* One-liner description for quick scanning
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Subtype
|
||||
*
|
||||
* Type of mental model: 'pinned' (observations LLM-generated) or 'directive' (observations user-provided)
|
||||
*/
|
||||
subtype?: string;
|
||||
/**
|
||||
* Observations
|
||||
*
|
||||
* For directives only: list of user-provided observations. Required when subtype='directive'.
|
||||
*/
|
||||
observations?: Array<ObservationInput> | null;
|
||||
/**
|
||||
* Tags
|
||||
*
|
||||
@@ -842,38 +830,6 @@ export type MemoryItem = {
|
||||
tags?: Array<string> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* MentalModelFreshnessResponse
|
||||
*
|
||||
* Freshness information for a mental model.
|
||||
*/
|
||||
export type MentalModelFreshnessResponse = {
|
||||
/**
|
||||
* Is Up To Date
|
||||
*
|
||||
* Whether the model has been refreshed since the last memory was added
|
||||
*/
|
||||
is_up_to_date: boolean;
|
||||
/**
|
||||
* Last Refresh At
|
||||
*
|
||||
* When the model was last refreshed (ISO format)
|
||||
*/
|
||||
last_refresh_at: string | null;
|
||||
/**
|
||||
* Memories Since Refresh
|
||||
*
|
||||
* Number of memories added since last refresh
|
||||
*/
|
||||
memories_since_refresh: number;
|
||||
/**
|
||||
* Reasons
|
||||
*
|
||||
* Reasons why the model needs refresh (empty if up to date). Possible values: never_refreshed, new_memories, mission_changed, disposition_changed, directives_changed
|
||||
*/
|
||||
reasons?: Array<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* MentalModelListResponse
|
||||
*
|
||||
@@ -889,53 +845,27 @@ export type MentalModelListResponse = {
|
||||
/**
|
||||
* MentalModelObservationResponse
|
||||
*
|
||||
* An observation within a mental model with its supporting evidence.
|
||||
* An observation within a mental model with its supporting memories.
|
||||
*/
|
||||
export type MentalModelObservationResponse = {
|
||||
/**
|
||||
* Title
|
||||
*
|
||||
* Short summary title for the observation
|
||||
* Observation header (empty for intro)
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Content
|
||||
* Text
|
||||
*
|
||||
* The observation content - detailed explanation
|
||||
* Observation content
|
||||
*/
|
||||
content: string;
|
||||
text: string;
|
||||
/**
|
||||
* Evidence
|
||||
* Based On
|
||||
*
|
||||
* Supporting evidence with quotes
|
||||
* Memory IDs supporting this observation
|
||||
*/
|
||||
evidence?: Array<ObservationEvidenceResponse>;
|
||||
/**
|
||||
* Created At
|
||||
*
|
||||
* When this observation was first created (ISO format)
|
||||
*/
|
||||
created_at: string;
|
||||
/**
|
||||
* Trend
|
||||
*
|
||||
* Computed trend: stable, strengthening, weakening, new, stale
|
||||
*/
|
||||
trend: string;
|
||||
/**
|
||||
* Evidence Count
|
||||
*
|
||||
* Number of evidence items supporting this observation
|
||||
*/
|
||||
evidence_count: number;
|
||||
/**
|
||||
* Evidence Span
|
||||
*
|
||||
* Time span of evidence: {from: iso_date, to: iso_date}
|
||||
*/
|
||||
evidence_span: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
based_on?: Array<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -970,12 +900,6 @@ export type MentalModelResponse = {
|
||||
* Structured observations with per-observation fact attribution
|
||||
*/
|
||||
observations?: Array<MentalModelObservationResponse>;
|
||||
/**
|
||||
* Version
|
||||
*
|
||||
* Version number of the mental model observations
|
||||
*/
|
||||
version?: number;
|
||||
/**
|
||||
* Entity Id
|
||||
*/
|
||||
@@ -992,74 +916,12 @@ export type MentalModelResponse = {
|
||||
* Last Updated
|
||||
*/
|
||||
last_updated?: string | null;
|
||||
/**
|
||||
* Last Refresh At
|
||||
*
|
||||
* When observations were last refreshed (ISO format)
|
||||
*/
|
||||
last_refresh_at?: string | null;
|
||||
/**
|
||||
* Freshness info (null for directive subtypes which don't need refresh)
|
||||
*/
|
||||
freshness?: MentalModelFreshnessResponse | null;
|
||||
/**
|
||||
* Created At
|
||||
*/
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* ObservationEvidenceResponse
|
||||
*
|
||||
* A single piece of evidence supporting an observation.
|
||||
*/
|
||||
export type ObservationEvidenceResponse = {
|
||||
/**
|
||||
* Memory Id
|
||||
*
|
||||
* ID of the memory unit this evidence comes from
|
||||
*/
|
||||
memory_id: string;
|
||||
/**
|
||||
* Quote
|
||||
*
|
||||
* Exact quote from the memory supporting the observation
|
||||
*/
|
||||
quote: string;
|
||||
/**
|
||||
* Relevance
|
||||
*
|
||||
* Brief explanation of how this quote supports the observation
|
||||
*/
|
||||
relevance: string;
|
||||
/**
|
||||
* Timestamp
|
||||
*
|
||||
* When the source memory was created (ISO format)
|
||||
*/
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* ObservationInput
|
||||
*
|
||||
* Input model for a single observation.
|
||||
*/
|
||||
export type ObservationInput = {
|
||||
/**
|
||||
* Title
|
||||
*
|
||||
* Short title/header for the observation
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Content
|
||||
*
|
||||
* Content of the observation
|
||||
*/
|
||||
content: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* OperationResponse
|
||||
*
|
||||
@@ -1408,15 +1270,21 @@ export type ReflectMentalModel = {
|
||||
/**
|
||||
* Subtype
|
||||
*
|
||||
* Mental model subtype: structural, emergent, learned, directive
|
||||
* Mental model subtype: structural, emergent, learned
|
||||
*/
|
||||
subtype: string;
|
||||
/**
|
||||
* Observations
|
||||
* Description
|
||||
*
|
||||
* Observations for directive mental models (subtype='directive')
|
||||
* Brief description
|
||||
*/
|
||||
observations?: Array<string> | null;
|
||||
description: string;
|
||||
/**
|
||||
* Summary
|
||||
*
|
||||
* Full summary (when looked up in detail)
|
||||
*/
|
||||
summary?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1568,12 +1436,6 @@ export type ReflectTrace = {
|
||||
* LLM calls made during reflection
|
||||
*/
|
||||
llm_calls?: Array<ReflectLlmCall>;
|
||||
/**
|
||||
* Mental Models
|
||||
*
|
||||
* Mental models used during reflection (includes directives with subtype='directive')
|
||||
*/
|
||||
mental_models?: Array<ReflectMentalModel>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1728,26 +1590,6 @@ export type UpdateDispositionRequest = {
|
||||
disposition: DispositionTraits;
|
||||
};
|
||||
|
||||
/**
|
||||
* UpdateMentalModelRequest
|
||||
*
|
||||
* Request model for updating a mental model.
|
||||
*/
|
||||
export type UpdateMentalModelRequest = {
|
||||
/**
|
||||
* Name
|
||||
*
|
||||
* New name for the mental model
|
||||
*/
|
||||
name?: string | null;
|
||||
/**
|
||||
* Description
|
||||
*
|
||||
* New description/rule text
|
||||
*/
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* ValidationError
|
||||
*/
|
||||
@@ -2384,48 +2226,6 @@ export type GetMentalModelResponses = {
|
||||
export type GetMentalModelResponse =
|
||||
GetMentalModelResponses[keyof GetMentalModelResponses];
|
||||
|
||||
export type UpdateMentalModelData = {
|
||||
body: UpdateMentalModelRequest;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
/**
|
||||
* Model Id
|
||||
*/
|
||||
model_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/mental-models/{model_id}";
|
||||
};
|
||||
|
||||
export type UpdateMentalModelErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type UpdateMentalModelError =
|
||||
UpdateMentalModelErrors[keyof UpdateMentalModelErrors];
|
||||
|
||||
export type UpdateMentalModelResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: MentalModelResponse;
|
||||
};
|
||||
|
||||
export type UpdateMentalModelResponse =
|
||||
UpdateMentalModelResponses[keyof UpdateMentalModelResponses];
|
||||
|
||||
export type RefreshMentalModelsData = {
|
||||
/**
|
||||
* Body
|
||||
@@ -2467,7 +2267,7 @@ export type RefreshMentalModelsResponses = {
|
||||
export type RefreshMentalModelsResponse =
|
||||
RefreshMentalModelsResponses[keyof RefreshMentalModelsResponses];
|
||||
|
||||
export type RefreshMentalModelData = {
|
||||
export type GenerateMentalModelData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
/**
|
||||
@@ -2486,110 +2286,28 @@ export type RefreshMentalModelData = {
|
||||
model_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/mental-models/{model_id}/refresh";
|
||||
url: "/v1/default/banks/{bank_id}/mental-models/{model_id}/generate";
|
||||
};
|
||||
|
||||
export type RefreshMentalModelErrors = {
|
||||
export type GenerateMentalModelErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type RefreshMentalModelError =
|
||||
RefreshMentalModelErrors[keyof RefreshMentalModelErrors];
|
||||
export type GenerateMentalModelError =
|
||||
GenerateMentalModelErrors[keyof GenerateMentalModelErrors];
|
||||
|
||||
export type RefreshMentalModelResponses = {
|
||||
export type GenerateMentalModelResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: AsyncOperationSubmitResponse;
|
||||
};
|
||||
|
||||
export type RefreshMentalModelResponse =
|
||||
RefreshMentalModelResponses[keyof RefreshMentalModelResponses];
|
||||
|
||||
export type ListMentalModelVersionsData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
/**
|
||||
* Model Id
|
||||
*/
|
||||
model_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/mental-models/{model_id}/versions";
|
||||
};
|
||||
|
||||
export type ListMentalModelVersionsErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ListMentalModelVersionsError =
|
||||
ListMentalModelVersionsErrors[keyof ListMentalModelVersionsErrors];
|
||||
|
||||
export type ListMentalModelVersionsResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type GetMentalModelVersionData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
/**
|
||||
* Model Id
|
||||
*/
|
||||
model_id: string;
|
||||
/**
|
||||
* Version
|
||||
*/
|
||||
version: number;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/mental-models/{model_id}/versions/{version}";
|
||||
};
|
||||
|
||||
export type GetMentalModelVersionErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetMentalModelVersionError =
|
||||
GetMentalModelVersionErrors[keyof GetMentalModelVersionErrors];
|
||||
|
||||
export type GetMentalModelVersionResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
export type GenerateMentalModelResponse =
|
||||
GenerateMentalModelResponses[keyof GenerateMentalModelResponses];
|
||||
|
||||
export type ListDocumentsData = {
|
||||
body?: never;
|
||||
|
||||
@@ -102,8 +102,6 @@ export class HindsightClient {
|
||||
documentId?: string;
|
||||
async?: boolean;
|
||||
entities?: EntityInput[];
|
||||
/** Optional list of tags for this memory */
|
||||
tags?: string[];
|
||||
}
|
||||
): Promise<RetainResponse> {
|
||||
const item: {
|
||||
@@ -113,7 +111,6 @@ export class HindsightClient {
|
||||
metadata?: Record<string, string>;
|
||||
document_id?: string;
|
||||
entities?: EntityInput[];
|
||||
tags?: string[];
|
||||
} = { content };
|
||||
if (options?.timestamp) {
|
||||
item.timestamp =
|
||||
@@ -133,9 +130,6 @@ export class HindsightClient {
|
||||
if (options?.entities) {
|
||||
item.entities = options.entities;
|
||||
}
|
||||
if (options?.tags) {
|
||||
item.tags = options.tags;
|
||||
}
|
||||
|
||||
const response = await sdk.retainMemories({
|
||||
client: this.client,
|
||||
@@ -198,10 +192,6 @@ export class HindsightClient {
|
||||
maxEntityTokens?: number;
|
||||
includeChunks?: boolean;
|
||||
maxChunkTokens?: number;
|
||||
/** Optional list of tags to filter memories by */
|
||||
tags?: string[];
|
||||
/** How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any' */
|
||||
tagsMatch?: 'any' | 'all' | 'any_strict' | 'all_strict';
|
||||
}
|
||||
): Promise<RecallResponse> {
|
||||
const response = await sdk.recallMemories({
|
||||
@@ -218,8 +208,6 @@ export class HindsightClient {
|
||||
entities: options?.includeEntities ? { max_tokens: options?.maxEntityTokens ?? 500 } : undefined,
|
||||
chunks: options?.includeChunks ? { max_tokens: options?.maxChunkTokens ?? 8192 } : undefined,
|
||||
},
|
||||
tags: options?.tags,
|
||||
tags_match: options?.tagsMatch,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -232,14 +220,7 @@ export class HindsightClient {
|
||||
async reflect(
|
||||
bankId: string,
|
||||
query: string,
|
||||
options?: {
|
||||
context?: string;
|
||||
budget?: Budget;
|
||||
/** Optional list of tags to filter memories by */
|
||||
tags?: string[];
|
||||
/** How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any' */
|
||||
tagsMatch?: 'any' | 'all' | 'any_strict' | 'all_strict';
|
||||
}
|
||||
options?: { context?: string; budget?: Budget }
|
||||
): Promise<ReflectResponse> {
|
||||
const response = await sdk.reflect({
|
||||
client: this.client,
|
||||
@@ -248,8 +229,6 @@ export class HindsightClient {
|
||||
query,
|
||||
context: options?.context,
|
||||
budget: options?.budget || 'low',
|
||||
tags: options?.tags,
|
||||
tags_match: options?.tagsMatch,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
|
||||
+5
-5
@@ -16,19 +16,19 @@ export async function POST(
|
||||
return NextResponse.json({ error: "model_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const response = await sdk.refreshMentalModel({
|
||||
const response = await sdk.generateMentalModel({
|
||||
client: lowLevelClient,
|
||||
path: { bank_id: bankId, model_id: modelId },
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
console.error("API error refreshing mental model:", response.error);
|
||||
return NextResponse.json({ error: "Failed to refresh mental model" }, { status: 500 });
|
||||
console.error("API error generating mental model:", response.error);
|
||||
return NextResponse.json({ error: "Failed to generate mental model" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(response.data, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error refreshing mental model:", error);
|
||||
return NextResponse.json({ error: "Failed to refresh mental model" }, { status: 500 });
|
||||
console.error("Error generating mental model:", error);
|
||||
return NextResponse.json({ error: "Failed to generate mental model" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { sdk, lowLevelClient } from "@/lib/hindsight-client";
|
||||
|
||||
const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://localhost:8888";
|
||||
|
||||
export async function PATCH(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ bankId: string; modelId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { bankId, modelId } = await params;
|
||||
|
||||
if (!bankId) {
|
||||
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!modelId) {
|
||||
return NextResponse.json({ error: "model_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Call the dataplane API directly since SDK may not have the update method yet
|
||||
const response = await fetch(
|
||||
`${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models/${modelId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("API error updating mental model:", errorText);
|
||||
return NextResponse.json(
|
||||
{ error: errorText || "Failed to update mental model" },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error updating mental model:", error);
|
||||
return NextResponse.json({ error: "Failed to update mental model" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ bankId: string; modelId: string }> }
|
||||
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://localhost:8888";
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ bankId: string; modelId: string; version: string }> }
|
||||
) {
|
||||
try {
|
||||
const { bankId, modelId, version } = await params;
|
||||
|
||||
if (!bankId) {
|
||||
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!modelId) {
|
||||
return NextResponse.json({ error: "model_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
return NextResponse.json({ error: "version is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models/${modelId}/versions/${version}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("API error getting mental model version:", errorText);
|
||||
return NextResponse.json(
|
||||
{ error: errorText || "Failed to get mental model version" },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error getting mental model version:", error);
|
||||
return NextResponse.json({ error: "Failed to get mental model version" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://localhost:8888";
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ bankId: string; modelId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { bankId, modelId } = await params;
|
||||
|
||||
if (!bankId) {
|
||||
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!modelId) {
|
||||
return NextResponse.json({ error: "model_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models/${modelId}/versions`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("API error listing mental model versions:", errorText);
|
||||
return NextResponse.json(
|
||||
{ error: errorText || "Failed to list mental model versions" },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error listing mental model versions:", error);
|
||||
return NextResponse.json({ error: "Failed to list mental model versions" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -6,34 +6,11 @@ const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://loca
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
|
||||
try {
|
||||
const { bankId } = await params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const subtype = searchParams.get("subtype");
|
||||
|
||||
if (!bankId) {
|
||||
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// If subtype is specified, call the dataplane API directly with the query param
|
||||
if (subtype) {
|
||||
const response = await fetch(
|
||||
`${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models?subtype=${subtype}`,
|
||||
{ method: "GET" }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("API error listing mental models:", errorText);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to list mental models" },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: 200 });
|
||||
}
|
||||
|
||||
// Default: use SDK which excludes directives
|
||||
const response = await sdk.listMentalModels({
|
||||
client: lowLevelClient,
|
||||
path: { bank_id: bankId },
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
Settings2,
|
||||
Eye,
|
||||
EyeOff,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Table,
|
||||
@@ -249,9 +248,11 @@ export function DataView({ factType }: DataViewProps) {
|
||||
return (
|
||||
<div>
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<RefreshCw className="w-8 h-8 mx-auto mb-3 text-muted-foreground animate-spin" />
|
||||
<p className="text-muted-foreground">Loading memories...</p>
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">⏳</div>
|
||||
<div className="text-sm text-muted-foreground">Loading memories...</div>
|
||||
</div>
|
||||
</div>
|
||||
) : data ? (
|
||||
<>
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { client } from "@/lib/api";
|
||||
import { useBank } from "@/lib/bank-context";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Loader2, Calendar, Tag, Users, FileText, Layers } from "lucide-react";
|
||||
|
||||
interface MemoryDetail {
|
||||
id: string;
|
||||
text: string;
|
||||
context: string;
|
||||
date: string;
|
||||
type: string;
|
||||
mentioned_at: string | null;
|
||||
occurred_start: string | null;
|
||||
occurred_end: string | null;
|
||||
entities: string[];
|
||||
document_id: string | null;
|
||||
chunk_id: string | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface MemoryDetailModalProps {
|
||||
memoryId: string | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function MemoryDetailModal({ memoryId, onClose }: MemoryDetailModalProps) {
|
||||
const { currentBank } = useBank();
|
||||
const [memory, setMemory] = useState<MemoryDetail | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState("memory");
|
||||
|
||||
// Document and chunk data
|
||||
const [document, setDocument] = useState<any>(null);
|
||||
const [chunk, setChunk] = useState<any>(null);
|
||||
const [loadingDocument, setLoadingDocument] = useState(false);
|
||||
const [loadingChunk, setLoadingChunk] = useState(false);
|
||||
|
||||
// Load memory details
|
||||
useEffect(() => {
|
||||
if (!memoryId || !currentBank) return;
|
||||
|
||||
const loadMemory = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setMemory(null);
|
||||
setDocument(null);
|
||||
setChunk(null);
|
||||
setActiveTab("memory");
|
||||
|
||||
try {
|
||||
const data = await client.getMemory(memoryId, currentBank);
|
||||
setMemory(data);
|
||||
} catch (err) {
|
||||
console.error("Error loading memory:", err);
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadMemory();
|
||||
}, [memoryId, currentBank]);
|
||||
|
||||
// Load document when tab is selected
|
||||
useEffect(() => {
|
||||
if (activeTab !== "document" || !memory?.document_id || !currentBank || document) return;
|
||||
|
||||
const loadDocument = async () => {
|
||||
setLoadingDocument(true);
|
||||
try {
|
||||
const data = await client.getDocument(memory.document_id!, currentBank);
|
||||
setDocument(data);
|
||||
} catch (err) {
|
||||
console.error("Error loading document:", err);
|
||||
} finally {
|
||||
setLoadingDocument(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadDocument();
|
||||
}, [activeTab, memory?.document_id, currentBank, document]);
|
||||
|
||||
// Load chunk when tab is selected
|
||||
useEffect(() => {
|
||||
if (activeTab !== "chunk" || !memory?.chunk_id || chunk) return;
|
||||
|
||||
const loadChunk = async () => {
|
||||
setLoadingChunk(true);
|
||||
try {
|
||||
const data = await client.getChunk(memory.chunk_id!);
|
||||
setChunk(data);
|
||||
} catch (err) {
|
||||
console.error("Error loading chunk:", err);
|
||||
} finally {
|
||||
setLoadingChunk(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadChunk();
|
||||
}, [activeTab, memory?.chunk_id, chunk]);
|
||||
|
||||
const isOpen = memoryId !== null;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Memory Details</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center text-destructive">
|
||||
<div className="text-sm">Error: {error}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : memory ? (
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex-1 flex flex-col overflow-hidden"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="memory" className="flex items-center gap-1.5">
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
Memory
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="chunk"
|
||||
disabled={!memory.chunk_id}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
<Layers className="w-3.5 h-3.5" />
|
||||
Chunk
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="document"
|
||||
disabled={!memory.document_id}
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
Document
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex-1 overflow-y-auto mt-4">
|
||||
<TabsContent value="memory" className="mt-0 space-y-4">
|
||||
{/* Memory text */}
|
||||
<div className="p-4 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
|
||||
Memory Text
|
||||
</div>
|
||||
<p className="text-sm text-foreground leading-relaxed">{memory.text}</p>
|
||||
</div>
|
||||
|
||||
{/* Metadata grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||
Type
|
||||
</div>
|
||||
<div className="text-sm text-foreground capitalize">{memory.type}</div>
|
||||
</div>
|
||||
{memory.context && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||
Context
|
||||
</div>
|
||||
<div className="text-sm text-foreground">{memory.context}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
{(memory.mentioned_at || memory.occurred_start) && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{memory.mentioned_at && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1 flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
Mentioned At
|
||||
</div>
|
||||
<div className="text-sm text-foreground">
|
||||
{new Date(memory.mentioned_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{memory.occurred_start && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1 flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
Occurred
|
||||
</div>
|
||||
<div className="text-sm text-foreground">
|
||||
{new Date(memory.occurred_start).toLocaleDateString()}
|
||||
{memory.occurred_end && memory.occurred_end !== memory.occurred_start && (
|
||||
<> - {new Date(memory.occurred_end).toLocaleDateString()}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Entities */}
|
||||
{memory.entities && memory.entities.length > 0 && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2 flex items-center gap-1">
|
||||
<Users className="w-3 h-3" />
|
||||
Entities
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{memory.entities.map((entity, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-0.5 bg-background rounded text-xs text-foreground"
|
||||
>
|
||||
{entity}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{memory.tags && memory.tags.length > 0 && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2 flex items-center gap-1">
|
||||
<Tag className="w-3 h-3" />
|
||||
Tags
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{memory.tags.map((tag, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary rounded text-xs"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* IDs */}
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||
Memory ID
|
||||
</div>
|
||||
<code className="text-xs font-mono text-muted-foreground break-all">
|
||||
{memory.id}
|
||||
</code>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="chunk" className="mt-0 space-y-4">
|
||||
{loadingChunk ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : chunk ? (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||
Chunk Index
|
||||
</div>
|
||||
<div className="text-sm text-foreground">{chunk.chunk_index}</div>
|
||||
</div>
|
||||
{chunk.chunk_text && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||
Text Length
|
||||
</div>
|
||||
<div className="text-sm text-foreground">
|
||||
{chunk.chunk_text.length.toLocaleString()} chars
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{chunk.chunk_text && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
|
||||
Chunk Text
|
||||
</div>
|
||||
<div className="p-4 bg-muted rounded-lg border border-border max-h-[300px] overflow-y-auto">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono text-foreground">
|
||||
{chunk.chunk_text}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||
Chunk ID
|
||||
</div>
|
||||
<code className="text-xs font-mono text-muted-foreground break-all">
|
||||
{chunk.chunk_id}
|
||||
</code>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
No chunk data available
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="document" className="mt-0 space-y-4">
|
||||
{loadingDocument ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : document ? (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{document.created_at && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||
Created
|
||||
</div>
|
||||
<div className="text-sm text-foreground">
|
||||
{new Date(document.created_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||
Memory Units
|
||||
</div>
|
||||
<div className="text-sm text-foreground">{document.memory_unit_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{document.original_text && (
|
||||
<>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||
Text Length
|
||||
</div>
|
||||
<div className="text-sm text-foreground">
|
||||
{document.original_text.length.toLocaleString()} chars
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
|
||||
Original Text
|
||||
</div>
|
||||
<div className="p-4 bg-muted rounded-lg border border-border max-h-[300px] overflow-y-auto">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono text-foreground">
|
||||
{document.original_text}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||
Document ID
|
||||
</div>
|
||||
<code className="text-xs font-mono text-muted-foreground break-all">
|
||||
{document.id}
|
||||
</code>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
No document data available
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,21 +14,9 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Sparkles,
|
||||
Info,
|
||||
Tag,
|
||||
Clock,
|
||||
Database,
|
||||
Brain,
|
||||
MessageSquare,
|
||||
Shield,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Sparkles, Info, Tag, Clock, Database, Brain } from "lucide-react";
|
||||
import JsonView from "react18-json-view";
|
||||
import "react18-json-view/src/style.css";
|
||||
import { MemoryDetailPanel } from "./memory-detail-panel";
|
||||
|
||||
type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
|
||||
type ViewMode = "answer" | "trace" | "json";
|
||||
@@ -45,94 +33,6 @@ export function ThinkView() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tags, setTags] = useState("");
|
||||
const [tagsMatch, setTagsMatch] = useState<TagsMatch>("any");
|
||||
const [feedback, setFeedback] = useState("");
|
||||
const [feedbackSubmitting, setFeedbackSubmitting] = useState(false);
|
||||
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
|
||||
const [selectedMemory, setSelectedMemory] = useState<any | null>(null);
|
||||
const [selectedDirective, setSelectedDirective] = useState<any | null>(null);
|
||||
const [fullDirective, setFullDirective] = useState<any | null>(null);
|
||||
const [loadingDirective, setLoadingDirective] = useState(false);
|
||||
const [selectedMentalModel, setSelectedMentalModel] = useState<any | null>(null);
|
||||
const [fullMentalModel, setFullMentalModel] = useState<any | null>(null);
|
||||
const [loadingMentalModel, setLoadingMentalModel] = useState(false);
|
||||
|
||||
const FEEDBACK_DIRECTIVE_NAME = "General Feedback";
|
||||
|
||||
// Load full directive data when one is selected
|
||||
const handleSelectDirective = async (directive: any) => {
|
||||
setSelectedDirective(directive);
|
||||
setFullDirective(null);
|
||||
if (!currentBank || !directive?.id) return;
|
||||
|
||||
setLoadingDirective(true);
|
||||
try {
|
||||
const directives = await client.listDirectives(currentBank);
|
||||
const fullDir = directives.items?.find((d: any) => d.id === directive.id);
|
||||
setFullDirective(fullDir || directive);
|
||||
} catch (error) {
|
||||
console.error("Failed to load directive:", error);
|
||||
setFullDirective(directive); // Fall back to partial data
|
||||
} finally {
|
||||
setLoadingDirective(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load full mental model data when one is selected
|
||||
const handleSelectMentalModel = async (model: any) => {
|
||||
setSelectedMentalModel(model);
|
||||
setFullMentalModel(null);
|
||||
if (!currentBank || !model?.id) return;
|
||||
|
||||
setLoadingMentalModel(true);
|
||||
try {
|
||||
const models = await client.listMentalModels(currentBank);
|
||||
const fullModel = models.items?.find((m: any) => m.id === model.id);
|
||||
setFullMentalModel(fullModel || model);
|
||||
} catch (error) {
|
||||
console.error("Failed to load mental model:", error);
|
||||
setFullMentalModel(model); // Fall back to partial data
|
||||
} finally {
|
||||
setLoadingMentalModel(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submitFeedback = async () => {
|
||||
if (!currentBank || !feedback.trim()) return;
|
||||
|
||||
setFeedbackSubmitting(true);
|
||||
try {
|
||||
// Find existing "General Feedback" directive
|
||||
const directives = await client.listDirectives(currentBank);
|
||||
const existingDirective = directives.items?.find((d) => d.name === FEEDBACK_DIRECTIVE_NAME);
|
||||
|
||||
if (existingDirective) {
|
||||
// Append to existing directive description
|
||||
const newDescription = existingDirective.description
|
||||
? `${existingDirective.description}\n${feedback.trim()}`
|
||||
: feedback.trim();
|
||||
await client.updateMentalModel(currentBank, existingDirective.id, {
|
||||
description: newDescription,
|
||||
});
|
||||
} else {
|
||||
// Create new directive with observation
|
||||
await client.createMentalModel(currentBank, {
|
||||
name: FEEDBACK_DIRECTIVE_NAME,
|
||||
description: "User feedback for improving responses",
|
||||
subtype: "directive",
|
||||
observations: [{ title: "Feedback", content: feedback.trim() }],
|
||||
});
|
||||
}
|
||||
|
||||
setFeedback("");
|
||||
setFeedbackSubmitted(true);
|
||||
setTimeout(() => setFeedbackSubmitted(false), 3000);
|
||||
} catch (error) {
|
||||
console.error("Error submitting feedback:", error);
|
||||
alert("Error submitting feedback: " + (error as Error).message);
|
||||
} finally {
|
||||
setFeedbackSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const runReflect = async () => {
|
||||
if (!currentBank || !query) return;
|
||||
@@ -235,7 +135,7 @@ export function ThinkView() {
|
||||
checked={includeFacts}
|
||||
onCheckedChange={(c) => setIncludeFacts(c as boolean)}
|
||||
/>
|
||||
<span className="text-sm">Include Source</span>
|
||||
<span className="text-sm">Include Facts</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox
|
||||
@@ -388,50 +288,6 @@ export function ThinkView() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Feedback */}
|
||||
<Card className="border-blue-200 dark:border-blue-800">
|
||||
<CardHeader className="py-4">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
Provide Feedback
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Your feedback will be saved as a directive to improve future responses
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
{feedbackSubmitted ? (
|
||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
||||
<span className="text-lg">✓</span>
|
||||
<span className="text-sm font-medium">
|
||||
Feedback saved to {FEEDBACK_DIRECTIVE_NAME}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-3">
|
||||
<Textarea
|
||||
value={feedback}
|
||||
onChange={(e) => setFeedback(e.target.value)}
|
||||
placeholder="Enter your feedback here..."
|
||||
className="flex-1 min-h-[60px] resize-none"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
submitFeedback();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={submitFeedback}
|
||||
disabled={feedbackSubmitting || !feedback.trim()}
|
||||
className="self-end"
|
||||
>
|
||||
{feedbackSubmitting ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -521,17 +377,7 @@ export function ThinkView() {
|
||||
}> = [];
|
||||
|
||||
llmCalls.forEach((lc: any, idx: number) => {
|
||||
// Add tools for this iteration (using iteration field from tool trace)
|
||||
const iterTools = toolCalls.filter(
|
||||
(tc: any) => tc.iteration === idx + 1
|
||||
);
|
||||
// Determine if this is the final LLM call:
|
||||
// - scope includes "final", OR
|
||||
// - it's the last LLM call AND no tools were called after it
|
||||
const isLastLLMCall = idx === llmCalls.length - 1;
|
||||
const isFinal =
|
||||
lc.scope.includes("final") ||
|
||||
(isLastLLMCall && iterTools.length === 0);
|
||||
const isFinal = lc.scope.includes("final");
|
||||
const iterNum = isFinal ? llmCalls.length : idx + 1;
|
||||
|
||||
// Add LLM call
|
||||
@@ -542,6 +388,10 @@ export function ThinkView() {
|
||||
isFinal,
|
||||
});
|
||||
|
||||
// Add tools for this iteration (using iteration field from tool trace)
|
||||
const iterTools = toolCalls.filter(
|
||||
(tc: any) => tc.iteration === idx + 1
|
||||
);
|
||||
if (iterTools.length > 0) {
|
||||
timeline.push({
|
||||
type: "tools",
|
||||
@@ -667,11 +517,7 @@ export function ThinkView() {
|
||||
<CardTitle className="text-base">Based On</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
{(result.based_on?.memories?.length || 0) +
|
||||
(result.based_on?.mental_models?.filter(
|
||||
(m: any) => m.subtype !== "directive"
|
||||
)?.length || 0) +
|
||||
(result.trace?.mental_models?.filter((m: any) => m.subtype === "directive")
|
||||
?.length || 0)}{" "}
|
||||
(result.based_on?.mental_models?.length || 0)}{" "}
|
||||
items used
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
@@ -682,7 +528,7 @@ export function ThinkView() {
|
||||
<div>
|
||||
<p className="font-medium text-sm text-foreground">Not included</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Enable "Include Source" to see memories.
|
||||
Enable "Include Facts" to see memories.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -697,53 +543,10 @@ export function ThinkView() {
|
||||
(f: any) => f.type === "experience"
|
||||
);
|
||||
const opinionFacts = memories.filter((f: any) => f.type === "opinion");
|
||||
const mentalModels = (result.based_on?.mental_models || []).filter(
|
||||
(m: any) => m.subtype !== "directive"
|
||||
);
|
||||
const directives =
|
||||
result.trace?.mental_models?.filter(
|
||||
(m: any) => m.subtype === "directive"
|
||||
) || [];
|
||||
const mentalModels = result.based_on?.mental_models || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Directives */}
|
||||
{directives.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-foreground">
|
||||
<Shield className="w-3 h-3" />
|
||||
Directives ({directives.length})
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{directives.map((directive: any, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-2 bg-muted rounded text-xs cursor-pointer hover:bg-muted/80 transition-colors"
|
||||
onClick={() => handleSelectDirective(directive)}
|
||||
>
|
||||
<div className="font-medium">{directive.name}</div>
|
||||
{directive.observations &&
|
||||
directive.observations.length > 0 && (
|
||||
<ul className="mt-1 space-y-0.5">
|
||||
{directive.observations.map(
|
||||
(obs: string, j: number) => (
|
||||
<li
|
||||
key={j}
|
||||
className="text-[10px] text-muted-foreground flex items-start gap-1"
|
||||
>
|
||||
<span>•</span>
|
||||
<span>{obs}</span>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mental Models */}
|
||||
{mentalModels.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
@@ -753,12 +556,13 @@ export function ThinkView() {
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{mentalModels.map((model: any, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-2 bg-muted rounded text-xs cursor-pointer hover:bg-muted/80 transition-colors"
|
||||
onClick={() => handleSelectMentalModel(model)}
|
||||
>
|
||||
<div key={i} className="p-2 bg-muted rounded text-xs">
|
||||
<div className="font-medium">{model.name}</div>
|
||||
{model.description && (
|
||||
<div className="text-[10px] text-muted-foreground mt-1">
|
||||
{model.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -774,11 +578,7 @@ export function ThinkView() {
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{worldFacts.map((fact: any, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-2 bg-muted rounded text-xs cursor-pointer hover:bg-muted/80 transition-colors"
|
||||
onClick={() => setSelectedMemory(fact)}
|
||||
>
|
||||
<div key={i} className="p-2 bg-muted rounded text-xs">
|
||||
{fact.text}
|
||||
{fact.context && (
|
||||
<div className="text-[10px] text-muted-foreground mt-1">
|
||||
@@ -800,11 +600,7 @@ export function ThinkView() {
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{experienceFacts.map((fact: any, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-2 bg-muted rounded text-xs cursor-pointer hover:bg-muted/80 transition-colors"
|
||||
onClick={() => setSelectedMemory(fact)}
|
||||
>
|
||||
<div key={i} className="p-2 bg-muted rounded text-xs">
|
||||
{fact.text}
|
||||
{fact.context && (
|
||||
<div className="text-[10px] text-muted-foreground mt-1">
|
||||
@@ -826,11 +622,7 @@ export function ThinkView() {
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{opinionFacts.map((fact: any, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-2 bg-muted rounded text-xs cursor-pointer hover:bg-muted/80 transition-colors"
|
||||
onClick={() => setSelectedMemory(fact)}
|
||||
>
|
||||
<div key={i} className="p-2 bg-muted rounded text-xs">
|
||||
{fact.text}
|
||||
{fact.context && (
|
||||
<div className="text-[10px] text-muted-foreground mt-1">
|
||||
@@ -894,224 +686,6 @@ export function ThinkView() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Memory Detail Panel */}
|
||||
{selectedMemory && (
|
||||
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l shadow-2xl z-50 overflow-y-auto">
|
||||
<MemoryDetailPanel
|
||||
memory={selectedMemory}
|
||||
onClose={() => setSelectedMemory(null)}
|
||||
inPanel
|
||||
bankId={currentBank || undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Directive Detail Panel */}
|
||||
{selectedDirective && (
|
||||
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l shadow-2xl z-50 overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-5 h-5" />
|
||||
<h2 className="text-lg font-semibold">Directive</h2>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setSelectedDirective(null);
|
||||
setFullDirective(null);
|
||||
}}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{loadingDirective ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Name</h3>
|
||||
<p className="mt-1 font-medium">
|
||||
{fullDirective?.name || selectedDirective.name}
|
||||
</p>
|
||||
</div>
|
||||
{fullDirective?.description && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Description</h3>
|
||||
<p className="mt-1 text-sm">{fullDirective.description}</p>
|
||||
</div>
|
||||
)}
|
||||
{fullDirective?.tags && fullDirective.tags.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-1">Tags</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{fullDirective.tags.map((tag: string) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground flex items-center gap-1"
|
||||
>
|
||||
<Tag className="w-2.5 h-2.5" />
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(fullDirective?.observations || selectedDirective.observations) && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-2">
|
||||
Observations (
|
||||
{(fullDirective?.observations || selectedDirective.observations)?.length || 0}
|
||||
)
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{(fullDirective?.observations || selectedDirective.observations)?.map(
|
||||
(obs: any, i: number) => (
|
||||
<div key={i} className="p-3 bg-muted rounded-lg">
|
||||
{obs.title && (
|
||||
<div className="font-medium text-sm mb-1">{obs.title}</div>
|
||||
)}
|
||||
<div className="text-sm text-muted-foreground whitespace-pre-wrap">
|
||||
{obs.content || obs.text || (typeof obs === "string" ? obs : "")}
|
||||
</div>
|
||||
{obs.memory_ids && obs.memory_ids.length > 0 && (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
Based on {obs.memory_ids.length} memories
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-2 border-t">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">ID</h3>
|
||||
<p className="mt-1 font-mono text-xs text-muted-foreground">
|
||||
{selectedDirective.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mental Model Detail Panel */}
|
||||
{selectedMentalModel && (
|
||||
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l shadow-2xl z-50 overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain className="w-5 h-5" />
|
||||
<h2 className="text-lg font-semibold">Mental Model</h2>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setSelectedMentalModel(null);
|
||||
setFullMentalModel(null);
|
||||
}}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{loadingMentalModel ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Name</h3>
|
||||
<p className="mt-1 font-medium">
|
||||
{fullMentalModel?.name || selectedMentalModel.name}
|
||||
</p>
|
||||
</div>
|
||||
{fullMentalModel?.description && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Description</h3>
|
||||
<p className="mt-1 text-sm">{fullMentalModel.description}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Type</h3>
|
||||
<p className="mt-1 text-sm">{selectedMentalModel.type}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Subtype</h3>
|
||||
<span
|
||||
className={`inline-block mt-1 text-xs px-2 py-0.5 rounded ${
|
||||
selectedMentalModel.subtype === "structural"
|
||||
? "bg-blue-500/10 text-blue-600"
|
||||
: selectedMentalModel.subtype === "emergent"
|
||||
? "bg-emerald-500/10 text-emerald-600"
|
||||
: selectedMentalModel.subtype === "learned"
|
||||
? "bg-violet-500/10 text-violet-600"
|
||||
: selectedMentalModel.subtype === "directive"
|
||||
? "bg-rose-500/10 text-rose-600"
|
||||
: "bg-muted"
|
||||
}`}
|
||||
>
|
||||
{selectedMentalModel.subtype}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{fullMentalModel?.tags && fullMentalModel.tags.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-1">Tags</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{fullMentalModel.tags.map((tag: string) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground flex items-center gap-1"
|
||||
>
|
||||
<Tag className="w-2.5 h-2.5" />
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{fullMentalModel?.observations && fullMentalModel.observations.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-2">
|
||||
Observations ({fullMentalModel.observations.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{fullMentalModel.observations.map((obs: any, i: number) => (
|
||||
<div key={i} className="p-3 bg-muted rounded-lg">
|
||||
{obs.title && <div className="font-medium text-sm mb-1">{obs.title}</div>}
|
||||
<div className="text-sm text-muted-foreground whitespace-pre-wrap">
|
||||
{obs.content || obs.text || (typeof obs === "string" ? obs : "")}
|
||||
</div>
|
||||
{obs.memory_ids && obs.memory_ids.length > 0 && (
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
Based on {obs.memory_ids.length} memories
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-2 border-t">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">ID</h3>
|
||||
<p className="mt-1 font-mono text-xs text-muted-foreground">
|
||||
{selectedMentalModel.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -272,7 +272,7 @@ export class ControlPlaneClient {
|
||||
subtype: string;
|
||||
name: string;
|
||||
description: string;
|
||||
observations?: Array<{ title: string; content: string; based_on: string[] }>;
|
||||
observations?: Array<{ title: string; text: string; based_on: string[] }>;
|
||||
entity_id: string | null;
|
||||
links: string[];
|
||||
tags?: string[];
|
||||
@@ -308,32 +308,6 @@ export class ControlPlaneClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a mental model (name and/or description)
|
||||
*/
|
||||
async updateMentalModel(
|
||||
bankId: string,
|
||||
modelId: string,
|
||||
params: { name?: string; description?: string }
|
||||
) {
|
||||
return this.fetchApi<{
|
||||
id: string;
|
||||
bank_id: string;
|
||||
subtype: string;
|
||||
name: string;
|
||||
description: string;
|
||||
observations?: Array<{ title: string; content: string; based_on: string[] }>;
|
||||
entity_id: string | null;
|
||||
links: string[];
|
||||
tags?: string[];
|
||||
last_updated: string | null;
|
||||
created_at: string;
|
||||
}>(`/api/banks/${bankId}/mental-models/${modelId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operation status
|
||||
*/
|
||||
@@ -350,11 +324,11 @@ export class ControlPlaneClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh content for a specific mental model (async)
|
||||
* Generate/refresh content for a specific mental model (async)
|
||||
*/
|
||||
async refreshMentalModel(bankId: string, modelId: string) {
|
||||
async generateMentalModel(bankId: string, modelId: string) {
|
||||
return this.fetchApi<{ operation_id: string; message: string }>(
|
||||
`/api/banks/${bankId}/mental-models/${modelId}/refresh`,
|
||||
`/api/banks/${bankId}/mental-models/${modelId}/generate`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
@@ -362,15 +336,13 @@ export class ControlPlaneClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mental model (pinned or directive)
|
||||
* Create a pinned mental model
|
||||
*/
|
||||
async createMentalModel(
|
||||
bankId: string,
|
||||
params: {
|
||||
name: string;
|
||||
description: string;
|
||||
subtype?: "pinned" | "directive";
|
||||
observations?: Array<{ title: string; content: string }>;
|
||||
tags?: string[];
|
||||
}
|
||||
) {
|
||||
@@ -380,7 +352,7 @@ export class ControlPlaneClient {
|
||||
subtype: string;
|
||||
name: string;
|
||||
description: string;
|
||||
observations?: Array<{ title: string; content: string; based_on: string[] }>;
|
||||
observations?: Array<{ title: string; text: string; based_on: string[] }>;
|
||||
entity_id: string | null;
|
||||
links: string[];
|
||||
tags?: string[];
|
||||
@@ -412,64 +384,6 @@ export class ControlPlaneClient {
|
||||
body: JSON.stringify(profile),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List directives for a bank
|
||||
*/
|
||||
async listDirectives(bankId: string) {
|
||||
return this.fetchApi<{
|
||||
items: Array<{
|
||||
id: string;
|
||||
bank_id: string;
|
||||
subtype: string;
|
||||
name: string;
|
||||
description: string;
|
||||
observations?: Array<{ title: string; content: string; based_on: string[] }>;
|
||||
entity_id: string | null;
|
||||
links: string[];
|
||||
tags?: string[];
|
||||
last_updated: string | null;
|
||||
created_at: string;
|
||||
}>;
|
||||
}>(`/api/banks/${bankId}/mental-models?subtype=directive`);
|
||||
}
|
||||
|
||||
/**
|
||||
* List version history for a mental model
|
||||
*/
|
||||
async listMentalModelVersions(bankId: string, modelId: string) {
|
||||
return this.fetchApi<{
|
||||
versions: Array<{
|
||||
version: number;
|
||||
created_at: string | null;
|
||||
observation_count: number;
|
||||
}>;
|
||||
}>(`/api/banks/${bankId}/mental-models/${modelId}/versions`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific version of a mental model
|
||||
*/
|
||||
async getMentalModelVersion(bankId: string, modelId: string, version: number) {
|
||||
return this.fetchApi<{
|
||||
version: number;
|
||||
observations: Array<{
|
||||
title: string;
|
||||
content: string;
|
||||
evidence: Array<{
|
||||
memory_id: string;
|
||||
quote: string;
|
||||
relevance: string;
|
||||
timestamp: string;
|
||||
}>;
|
||||
created_at: string;
|
||||
trend: string;
|
||||
evidence_count: number;
|
||||
evidence_span: { from: string | null; to: string | null };
|
||||
}>;
|
||||
created_at: string | null;
|
||||
}>(`/api/banks/${bankId}/mental-models/${modelId}/versions/${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
|
||||
@@ -43,13 +43,11 @@ Make sure you've completed the [Quick Start](./quickstart) to install the client
|
||||
|-----------|------|---------|-------------|
|
||||
| `query` | string | required | Natural language query |
|
||||
| `types` | list | all | Filter: `world`, `experience`, `opinion` |
|
||||
| `budget` | string | "mid" | Budget level: `low`, `mid`, `high` |
|
||||
| `budget` | string | "mid" | Budget level: "low", "mid", "high" |
|
||||
| `max_tokens` | int | 4096 | Token budget for results |
|
||||
| `trace` | bool | false | Enable trace output for debugging |
|
||||
| `include_entities` | bool | false | Include entity observations |
|
||||
| `max_entity_tokens` | int | 500 | Token budget for entity observations |
|
||||
| `tags` | list | None | Filter memories by tags (see [Tag Filtering](#filter-by-tags)) |
|
||||
| `tags_match` | string | "any" | How to match tags: `any`, `all`, `any_strict`, `all_strict` |
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
@@ -129,51 +127,3 @@ The `budget` parameter controls graph traversal depth:
|
||||
<CodeSnippet code={recallMjs} section="recall-budget-levels" language="javascript" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Filter by Tags
|
||||
|
||||
Tags enable **visibility scoping**—filter memories based on tags assigned during [retain](./retain#tagging-memories). This is essential for multi-user agents where each user should only see their own memories.
|
||||
|
||||
### Basic Tag Filtering
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={recallPy} section="recall-with-tags" language="python" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Tag Match Modes
|
||||
|
||||
The `tags_match` parameter controls how tags are matched:
|
||||
|
||||
| Mode | Behavior | Untagged Memories |
|
||||
|------|----------|-------------------|
|
||||
| `any` | OR: memory has ANY of the specified tags | **Included** |
|
||||
| `all` | AND: memory has ALL of the specified tags | **Included** |
|
||||
| `any_strict` | OR: memory has ANY of the specified tags | **Excluded** |
|
||||
| `all_strict` | AND: memory has ALL of the specified tags | **Excluded** |
|
||||
|
||||
**Strict modes** are useful when you want to ensure only tagged memories are returned:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={recallPy} section="recall-tags-strict" language="python" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
**AND matching** requires all specified tags to be present:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={recallPy} section="recall-tags-all" language="python" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Use Cases
|
||||
|
||||
| Scenario | Tags | Mode | Result |
|
||||
|----------|------|------|--------|
|
||||
| User A's memories only | `["user:alice"]` | `any_strict` | Only memories tagged `user:alice` |
|
||||
| Support + feedback | `["support", "feedback"]` | `any` | Memories with either tag + untagged |
|
||||
| Multi-user room | `["user:alice", "room:general"]` | `all_strict` | Only memories with both tags |
|
||||
| Global + user-specific | `["user:alice"]` | `any` | Alice's memories + shared (untagged) |
|
||||
|
||||
@@ -50,12 +50,10 @@ Make sure you've completed the [Quick Start](./quickstart) to install the client
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `query` | string | required | Question or prompt |
|
||||
| `budget` | string | "low" | Budget level: `low`, `mid`, `high` |
|
||||
| `budget` | string | "low" | Budget level: "low", "mid", "high" |
|
||||
| `context` | string | None | Additional context for the query |
|
||||
| `max_tokens` | int | 4096 | Maximum tokens for the response |
|
||||
| `response_schema` | object | None | JSON Schema for [structured output](#structured-output) |
|
||||
| `tags` | list | None | Filter memories by tags during reflection |
|
||||
| `tags_match` | string | "any" | How to match tags: `any`, `all`, `any_strict`, `all_strict` |
|
||||
|
||||
### Response Fields
|
||||
|
||||
@@ -249,24 +247,3 @@ hindsight memory reflect hiring-team \
|
||||
- Use `model_validate()` to parse the response back into your Pydantic model
|
||||
- Keep schemas focused — extract only what you need
|
||||
- Use `Optional` fields for data that may not always be available
|
||||
|
||||
## Filter by Tags
|
||||
|
||||
Like [recall](./recall#filter-by-tags), reflect supports tag filtering to scope which memories are considered during reasoning. This is essential for multi-user scenarios where reflection should only consider memories relevant to a specific user.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={reflectPy} section="reflect-with-tags" language="python" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `tags_match` parameter works the same as in recall:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `any` | OR matching, includes untagged memories |
|
||||
| `all` | AND matching, includes untagged memories |
|
||||
| `any_strict` | OR matching, excludes untagged memories |
|
||||
| `all_strict` | AND matching, excludes untagged memories |
|
||||
|
||||
See [Retain API](./retain#tagging-memories) for how to tag memories and [Recall API](./recall#filter-by-tags) for more details on tag matching modes.
|
||||
|
||||
@@ -129,55 +129,3 @@ For large batches, use async ingestion to avoid blocking:
|
||||
<CodeSnippet code={retainMjs} section="retain-async" language="javascript" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Tagging Memories
|
||||
|
||||
Tags enable **visibility scoping**—useful when one memory bank serves multiple users but each should only see relevant memories. For example, an agent that chats with multiple users can tag memories by user ID and filter during recall.
|
||||
|
||||
### Tag Individual Items
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={retainPy} section="retain-with-tags" language="python" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Apply Tags to All Items in a Batch
|
||||
|
||||
Use `document_tags` to apply the same tags to all items in a request:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={retainPy} section="retain-with-document-tags" language="python" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
When both `document_tags` and item-level `tags` are provided, they are merged together.
|
||||
|
||||
### Tag Naming Conventions
|
||||
|
||||
Use consistent naming patterns for tags:
|
||||
|
||||
| Pattern | Example | Use Case |
|
||||
|---------|---------|----------|
|
||||
| `user:<id>` | `user:alice` | Multi-user agent filtering |
|
||||
| `session:<id>` | `session:123` | Session-based scoping |
|
||||
| `room:<id>` | `room:general` | Chat room isolation |
|
||||
| `topic:<name>` | `topic:feedback` | Topic categorization |
|
||||
|
||||
### Listing Tags
|
||||
|
||||
Use the list tags API to discover existing tags, useful for UI autocomplete or wildcard expansion:
|
||||
|
||||
```python
|
||||
# List all tags in a bank
|
||||
tags = client.list_tags(bank_id="my-bank")
|
||||
for tag in tags.items:
|
||||
print(f"{tag.tag}: {tag.count} memories")
|
||||
|
||||
# Search with wildcards (* matches any characters)
|
||||
user_tags = client.list_tags(bank_id="my-bank", q="user:*")
|
||||
admin_tags = client.list_tags(bank_id="my-bank", q="*-admin")
|
||||
```
|
||||
|
||||
See [Recall API](./recall#filter-by-tags) for filtering memories by tags during retrieval.
|
||||
|
||||
@@ -95,71 +95,29 @@ Converts text into dense vector representations for semantic similarity search.
|
||||
|
||||
**Default:** `BAAI/bge-small-en-v1.5` (384 dimensions, ~130MB)
|
||||
|
||||
### Supported Providers
|
||||
**Alternatives:**
|
||||
|
||||
| Provider | Description | Best For |
|
||||
|----------|-------------|----------|
|
||||
| `local` | SentenceTransformers (default) | Development, low latency |
|
||||
| `openai` | OpenAI embeddings API | Production, high quality |
|
||||
| `cohere` | Cohere embeddings API | Production, multilingual |
|
||||
| `tei` | HuggingFace Text Embeddings Inference | Production, self-hosted |
|
||||
| `litellm` | LiteLLM proxy (unified gateway) | Multi-provider setups |
|
||||
| Model | Use Case |
|
||||
|-------|----------|
|
||||
| `BAAI/bge-small-en-v1.5` | Default, fast, good quality |
|
||||
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | Multilingual (50+ languages) |
|
||||
|
||||
### Local Models
|
||||
|
||||
| Model | Dimensions | Use Case |
|
||||
|-------|------------|----------|
|
||||
| `BAAI/bge-small-en-v1.5` | 384 | Default, fast, good quality |
|
||||
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384 | Multilingual (50+ languages) |
|
||||
|
||||
### OpenAI Models
|
||||
|
||||
| Model | Dimensions | Use Case |
|
||||
|-------|------------|----------|
|
||||
| `text-embedding-3-small` | 1536 | Default OpenAI, cost-effective |
|
||||
| `text-embedding-3-large` | 3072 | Higher quality, more expensive |
|
||||
| `text-embedding-ada-002` | 1536 | Legacy model |
|
||||
|
||||
### Cohere Models
|
||||
|
||||
| Model | Dimensions | Use Case |
|
||||
|-------|------------|----------|
|
||||
| `embed-english-v3.0` | 1024 | English text |
|
||||
| `embed-multilingual-v3.0` | 1024 | 100+ languages |
|
||||
|
||||
:::warning Embedding Dimensions
|
||||
Hindsight automatically detects the embedding dimension at startup and adjusts the database schema. Once memories are stored, you cannot change dimensions without losing data.
|
||||
:::warning
|
||||
All embedding models must produce **384-dimensional vectors** to match the database schema.
|
||||
:::
|
||||
|
||||
**Configuration Examples:**
|
||||
**Configuration:**
|
||||
|
||||
```bash
|
||||
# Local provider (default)
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
|
||||
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
|
||||
|
||||
# OpenAI
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small
|
||||
|
||||
# Cohere
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0
|
||||
|
||||
# TEI (self-hosted)
|
||||
# TEI provider (remote)
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
|
||||
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
|
||||
|
||||
# LiteLLM proxy
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm
|
||||
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
|
||||
export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small
|
||||
```
|
||||
|
||||
See [Configuration](./configuration#embeddings) for all options including Azure OpenAI and custom endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Encoder (Reranker)
|
||||
@@ -168,18 +126,7 @@ Reranks initial search results to improve precision.
|
||||
|
||||
**Default:** `cross-encoder/ms-marco-MiniLM-L-6-v2` (~85MB)
|
||||
|
||||
### Supported Providers
|
||||
|
||||
| Provider | Description | Best For |
|
||||
|----------|-------------|----------|
|
||||
| `local` | SentenceTransformers CrossEncoder (default) | Development, low latency |
|
||||
| `cohere` | Cohere rerank API | Production, high quality |
|
||||
| `tei` | HuggingFace Text Embeddings Inference | Production, self-hosted |
|
||||
| `flashrank` | FlashRank (lightweight, fast) | Resource-constrained environments |
|
||||
| `litellm` | LiteLLM proxy (unified gateway) | Multi-provider setups |
|
||||
| `rrf` | RRF-only (no neural reranking) | Testing, minimal resources |
|
||||
|
||||
### Local Models
|
||||
**Alternatives:**
|
||||
|
||||
| Model | Use Case |
|
||||
|-------|----------|
|
||||
@@ -187,51 +134,14 @@ Reranks initial search results to improve precision.
|
||||
| `cross-encoder/ms-marco-MiniLM-L-12-v2` | Higher accuracy |
|
||||
| `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | Multilingual |
|
||||
|
||||
### Cohere Models
|
||||
|
||||
| Model | Use Case |
|
||||
|-------|----------|
|
||||
| `rerank-english-v3.0` | English text |
|
||||
| `rerank-multilingual-v3.0` | 100+ languages |
|
||||
|
||||
### LiteLLM Supported Providers
|
||||
|
||||
LiteLLM supports multiple reranking providers via the `/rerank` endpoint:
|
||||
|
||||
| Provider | Model Example |
|
||||
|----------|---------------|
|
||||
| Cohere | `cohere/rerank-english-v3.0` |
|
||||
| Together AI | `together_ai/...` |
|
||||
| Voyage AI | `voyage/rerank-2` |
|
||||
| Jina AI | `jina_ai/...` |
|
||||
| AWS Bedrock | `bedrock/...` |
|
||||
|
||||
**Configuration Examples:**
|
||||
**Configuration:**
|
||||
|
||||
```bash
|
||||
# Local provider (default)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=local
|
||||
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
|
||||
|
||||
# Cohere
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
|
||||
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
|
||||
|
||||
# TEI (self-hosted)
|
||||
# TEI provider (remote)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=tei
|
||||
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
|
||||
|
||||
# FlashRank (lightweight)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=flashrank
|
||||
|
||||
# LiteLLM proxy
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=litellm
|
||||
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
|
||||
export HINDSIGHT_API_RERANKER_LITELLM_MODEL=cohere/rerank-english-v3.0
|
||||
|
||||
# RRF-only (no neural reranking)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=rrf
|
||||
```
|
||||
|
||||
See [Configuration](./configuration#reranker) for all options including Azure-hosted endpoints and batch settings.
|
||||
|
||||
@@ -183,4 +183,4 @@ Disposition creates **consistent character** across conversations while allowing
|
||||
|
||||
- [**Retain**](./retain) — How rich facts are stored
|
||||
- [**Recall**](./retrieval) — How multi-strategy search works
|
||||
- [**Reflect API**](./api/reflect) — Code examples, parameters, and tag filtering
|
||||
- [API Reference: Reflect](./api/reflect) — Code examples and usage
|
||||
|
||||
@@ -169,13 +169,34 @@ As facts accumulate about an entity, Hindsight synthesizes **observations** —
|
||||
|
||||
## Tagging Memories
|
||||
|
||||
Tags enable visibility scoping—useful when one memory bank serves multiple users but each should only see relevant memories.
|
||||
You can tag memories for filtering during recall—useful when one memory bank serves multiple users but each user should only see relevant memories.
|
||||
|
||||
- **Item tags**: Tag individual memories with specific scopes
|
||||
- **Document tags**: Apply tags to all items in a batch
|
||||
- **Tag filtering**: Filter during recall/reflect by tags
|
||||
```python
|
||||
# Tag memories for specific users
|
||||
client.retain(
|
||||
bank_id="my-agent",
|
||||
items=[
|
||||
{
|
||||
"content": "Alice prefers morning meetings",
|
||||
"tags": ["user_alice"]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
See [Retain API](./api/retain) for code examples and [Recall API](./api/recall) for filtering options.
|
||||
# Apply tags to all items in a batch
|
||||
client.retain(
|
||||
bank_id="my-agent",
|
||||
document_tags=["session_123", "user_alice"], # Applied to all items
|
||||
items=[
|
||||
{"content": "Alice discussed the project timeline"},
|
||||
{"content": "Alice mentioned she needs help with Python"}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
During recall, use `tags_match` to control matching:
|
||||
- `"any"` (default): OR matching - returns memories where **any** tag overlaps
|
||||
- `"all"`: AND matching - returns memories containing **all** specified tags
|
||||
|
||||
---
|
||||
|
||||
@@ -198,4 +219,4 @@ All stored in your isolated **memory bank**, ready for `recall()` and `reflect()
|
||||
|
||||
- [**Recall**](./retrieval) — How multi-strategy search retrieves relevant memories
|
||||
- [**Reflect**](./reflect) — How disposition influences reasoning and opinion formation
|
||||
- [**Retain API**](./api/retain) — Code examples and parameters
|
||||
- [API Reference](./api/retain) — Code examples for retaining memories
|
||||
|
||||
@@ -133,9 +133,9 @@ Hindsight is built for AI agents, not humans. Traditional search systems return
|
||||
**Parameters you control:**
|
||||
- `max_tokens`: How much memory content to return (default: 4096 tokens)
|
||||
- `budget`: Search depth level (low, mid, high)
|
||||
- `types`: Filter by world, experience, opinion, or all
|
||||
- `tags`: Filter memories by visibility tags
|
||||
- `tags_match`: How to match tags (see [Recall API](./api/recall) for all options)
|
||||
- `fact_type`: Filter by world, experience, opinion, or all
|
||||
- `tags`: Filter memories by tags
|
||||
- `tags_match`: How to match tags - `"any"` for OR (default), `"all"` for AND
|
||||
|
||||
### Expanding Context: Chunks and Entity Observations
|
||||
|
||||
@@ -243,4 +243,3 @@ See [Configuration → Retrieval](./configuration#retrieval) for available algor
|
||||
|
||||
- [**Retain**](./retain) — How memories are stored with rich context
|
||||
- [**Reflect**](./reflect) — How disposition influences reasoning
|
||||
- [**Recall API**](./api/recall) — Code examples, parameters, and tag filtering
|
||||
|
||||
@@ -116,39 +116,6 @@ results = client.recall(bank_id="my-bank", query="How are Alice and Bob connecte
|
||||
# [/docs:recall-budget-levels]
|
||||
|
||||
|
||||
# [docs:recall-with-tags]
|
||||
# Filter recall to only memories tagged for a specific user
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What feedback did the user give?",
|
||||
tags=["user:alice"],
|
||||
tags_match="any" # OR matching, includes untagged (default)
|
||||
)
|
||||
# [/docs:recall-with-tags]
|
||||
|
||||
|
||||
# [docs:recall-tags-strict]
|
||||
# Strict mode: only return memories that have matching tags (exclude untagged)
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What did the user say?",
|
||||
tags=["user:alice"],
|
||||
tags_match="any_strict" # OR matching, excludes untagged memories
|
||||
)
|
||||
# [/docs:recall-tags-strict]
|
||||
|
||||
|
||||
# [docs:recall-tags-all]
|
||||
# AND matching: require ALL specified tags to be present
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What bugs were reported?",
|
||||
tags=["user:alice", "bug-report"],
|
||||
tags_match="all_strict" # Memory must have BOTH tags
|
||||
)
|
||||
# [/docs:recall-tags-all]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Cleanup (not shown in docs)
|
||||
# =============================================================================
|
||||
|
||||
@@ -81,17 +81,6 @@ for fact in response.based_on or []:
|
||||
# [/docs:reflect-sources]
|
||||
|
||||
|
||||
# [docs:reflect-with-tags]
|
||||
# Filter reflection to only consider memories for a specific user
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="What does this user think about our product?",
|
||||
tags=["user:alice"],
|
||||
tags_match="any_strict" # Only use memories tagged for this user
|
||||
)
|
||||
# [/docs:reflect-with-tags]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Cleanup (not shown in docs)
|
||||
# =============================================================================
|
||||
|
||||
@@ -67,39 +67,6 @@ print(result.var_async) # True
|
||||
# [/docs:retain-async]
|
||||
|
||||
|
||||
# [docs:retain-with-tags]
|
||||
# Tag individual items for visibility scoping
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{
|
||||
"content": "User Alice said she loves the new dashboard",
|
||||
"tags": ["user:alice", "feedback"]
|
||||
},
|
||||
{
|
||||
"content": "User Bob reported a bug in the search feature",
|
||||
"tags": ["user:bob", "bug-report"]
|
||||
}
|
||||
],
|
||||
document_id="user_feedback_001"
|
||||
)
|
||||
# [/docs:retain-with-tags]
|
||||
|
||||
|
||||
# [docs:retain-with-document-tags]
|
||||
# Apply tags to all items in a batch
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": "Alice mentioned she prefers dark mode"},
|
||||
{"content": "Bob asked about keyboard shortcuts"}
|
||||
],
|
||||
document_id="support_session_123",
|
||||
document_tags=["session:123", "support"] # Applied to all items
|
||||
)
|
||||
# [/docs:retain-with-document-tags]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Cleanup (not shown in docs)
|
||||
# =============================================================================
|
||||
|
||||
@@ -901,7 +901,7 @@
|
||||
"Mental Models"
|
||||
],
|
||||
"summary": "Create mental model",
|
||||
"description": "Create a mental model. Supports two subtypes:\n- 'pinned' (default): User-defined topic, observations are LLM-generated on refresh\n- 'directive': User-defined hard rules, observations are provided at creation and never regenerated",
|
||||
"description": "Create a pinned mental model. Pinned models are user-defined and persist across refreshes.",
|
||||
"operationId": "create_mental_model",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1096,82 +1096,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Mental Models"
|
||||
],
|
||||
"summary": "Update mental model",
|
||||
"description": "Update a mental model's name and/or description. Useful for editing directives.",
|
||||
"operationId": "update_mental_model",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "model_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Model Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateMentalModelRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MentalModelResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/mental-models/refresh": {
|
||||
@@ -1250,14 +1174,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/mental-models/{model_id}/refresh": {
|
||||
"/v1/default/banks/{bank_id}/mental-models/{model_id}/generate": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Mental Models"
|
||||
],
|
||||
"summary": "Refresh mental model content (async)",
|
||||
"description": "Submit a background job to refresh content for a specific mental model. This is useful for newly created learned models or to refresh content for any model.",
|
||||
"operationId": "refresh_mental_model",
|
||||
"summary": "Generate mental model content (async)",
|
||||
"description": "Submit a background job to generate/refresh content for a specific mental model. This is useful for newly created learned models or to regenerate content for any model.",
|
||||
"operationId": "generate_mental_model",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
@@ -1318,147 +1242,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/mental-models/{model_id}/versions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Mental Models"
|
||||
],
|
||||
"summary": "List mental model version history",
|
||||
"description": "List all saved versions of a mental model's observations, ordered by version descending.",
|
||||
"operationId": "list_mental_model_versions",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "model_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Model Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/mental-models/{model_id}/versions/{version}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Mental Models"
|
||||
],
|
||||
"summary": "Get specific mental model version",
|
||||
"description": "Get observations from a specific version of a mental model.",
|
||||
"operationId": "get_mental_model_version",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "model_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Model Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"title": "Version"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/documents": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -3116,27 +2899,6 @@
|
||||
"title": "Description",
|
||||
"description": "One-liner description for quick scanning"
|
||||
},
|
||||
"subtype": {
|
||||
"type": "string",
|
||||
"title": "Subtype",
|
||||
"description": "Type of mental model: 'pinned' (observations LLM-generated) or 'directive' (observations user-provided)",
|
||||
"default": "pinned"
|
||||
},
|
||||
"observations": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ObservationInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Observations",
|
||||
"description": "For directives only: list of user-provided observations. Required when subtype='directive'."
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -3152,27 +2914,14 @@
|
||||
"description"
|
||||
],
|
||||
"title": "CreateMentalModelRequest",
|
||||
"description": "Request model for creating a mental model.",
|
||||
"examples": [
|
||||
{
|
||||
"description": "Key product priorities and upcoming features",
|
||||
"name": "Product Roadmap",
|
||||
"tags": [
|
||||
"project-x"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Rules about scheduling meetings",
|
||||
"name": "Meeting Rules",
|
||||
"observations": [
|
||||
{
|
||||
"content": "Never schedule meetings before 10am",
|
||||
"title": "Morning meetings"
|
||||
}
|
||||
],
|
||||
"subtype": "directive"
|
||||
}
|
||||
]
|
||||
"description": "Request model for creating a pinned mental model.",
|
||||
"example": {
|
||||
"description": "Key product priorities and upcoming features",
|
||||
"name": "Product Roadmap",
|
||||
"tags": [
|
||||
"project-x"
|
||||
]
|
||||
}
|
||||
},
|
||||
"CreatedMentalModel": {
|
||||
"properties": {
|
||||
@@ -4063,48 +3812,6 @@
|
||||
"timestamp": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
},
|
||||
"MentalModelFreshnessResponse": {
|
||||
"properties": {
|
||||
"is_up_to_date": {
|
||||
"type": "boolean",
|
||||
"title": "Is Up To Date",
|
||||
"description": "Whether the model has been refreshed since the last memory was added"
|
||||
},
|
||||
"last_refresh_at": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Last Refresh At",
|
||||
"description": "When the model was last refreshed (ISO format)"
|
||||
},
|
||||
"memories_since_refresh": {
|
||||
"type": "integer",
|
||||
"title": "Memories Since Refresh",
|
||||
"description": "Number of memories added since last refresh"
|
||||
},
|
||||
"reasons": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Reasons",
|
||||
"description": "Reasons why the model needs refresh (empty if up to date). Possible values: never_refreshed, new_memories, mission_changed, disposition_changed, directives_changed"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"is_up_to_date",
|
||||
"last_refresh_at",
|
||||
"memories_since_refresh"
|
||||
],
|
||||
"title": "MentalModelFreshnessResponse",
|
||||
"description": "Freshness information for a mental model."
|
||||
},
|
||||
"MentalModelListResponse": {
|
||||
"properties": {
|
||||
"items": {
|
||||
@@ -4127,54 +3834,29 @@
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Title",
|
||||
"description": "Short summary title for the observation"
|
||||
"description": "Observation header (empty for intro)"
|
||||
},
|
||||
"content": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"title": "Content",
|
||||
"description": "The observation content - detailed explanation"
|
||||
"title": "Text",
|
||||
"description": "Observation content"
|
||||
},
|
||||
"evidence": {
|
||||
"based_on": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ObservationEvidenceResponse"
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Evidence",
|
||||
"description": "Supporting evidence with quotes"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"title": "Created At",
|
||||
"description": "When this observation was first created (ISO format)"
|
||||
},
|
||||
"trend": {
|
||||
"type": "string",
|
||||
"title": "Trend",
|
||||
"description": "Computed trend: stable, strengthening, weakening, new, stale"
|
||||
},
|
||||
"evidence_count": {
|
||||
"type": "integer",
|
||||
"title": "Evidence Count",
|
||||
"description": "Number of evidence items supporting this observation"
|
||||
},
|
||||
"evidence_span": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
"title": "Evidence Span",
|
||||
"description": "Time span of evidence: {from: iso_date, to: iso_date}"
|
||||
"title": "Based On",
|
||||
"description": "Memory IDs supporting this observation"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"title",
|
||||
"content",
|
||||
"created_at",
|
||||
"trend",
|
||||
"evidence_count",
|
||||
"evidence_span"
|
||||
"text"
|
||||
],
|
||||
"title": "MentalModelObservationResponse",
|
||||
"description": "An observation within a mental model with its supporting evidence."
|
||||
"description": "An observation within a mental model with its supporting memories."
|
||||
},
|
||||
"MentalModelResponse": {
|
||||
"properties": {
|
||||
@@ -4206,12 +3888,6 @@
|
||||
"title": "Observations",
|
||||
"description": "Structured observations with per-observation fact attribution"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"title": "Version",
|
||||
"description": "Version number of the mental model observations",
|
||||
"default": 0
|
||||
},
|
||||
"entity_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -4250,29 +3926,6 @@
|
||||
],
|
||||
"title": "Last Updated"
|
||||
},
|
||||
"last_refresh_at": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Last Refresh At",
|
||||
"description": "When observations were last refreshed (ISO format)"
|
||||
},
|
||||
"freshness": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/MentalModelFreshnessResponse"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Freshness info (null for directive subtypes which don't need refresh)"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"title": "Created At"
|
||||
@@ -4293,99 +3946,25 @@
|
||||
"bank_id": "test-bank",
|
||||
"created_at": "2024-01-10T08:00:00Z",
|
||||
"description": "Who's on the team and their roles",
|
||||
"freshness": {
|
||||
"is_up_to_date": true,
|
||||
"last_refresh_at": "2024-01-15T10:30:00Z",
|
||||
"memories_since_refresh": 0,
|
||||
"reasons": []
|
||||
},
|
||||
"id": "team-structure",
|
||||
"last_refresh_at": "2024-01-15T10:30:00Z",
|
||||
"last_updated": "2024-01-15T10:30:00Z",
|
||||
"links": [],
|
||||
"name": "Team Structure",
|
||||
"observations": [
|
||||
{
|
||||
"content": "The team prefers async communication over synchronous meetings",
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"evidence": [
|
||||
{
|
||||
"memory_id": "uuid1",
|
||||
"quote": "I prefer Slack over meetings",
|
||||
"relevance": "Shows async preference",
|
||||
"timestamp": "2024-01-10T08:00:00Z"
|
||||
}
|
||||
"based_on": [
|
||||
"uuid1"
|
||||
],
|
||||
"evidence_count": 1,
|
||||
"evidence_span": {
|
||||
"from": "2024-01-10T08:00:00Z",
|
||||
"to": "2024-01-10T08:00:00Z"
|
||||
},
|
||||
"title": "Prefers async communication",
|
||||
"trend": "stable"
|
||||
"text": "The team consists of...",
|
||||
"title": "Overview"
|
||||
}
|
||||
],
|
||||
"subtype": "structural",
|
||||
"tags": [
|
||||
"project-x"
|
||||
],
|
||||
"version": 1
|
||||
]
|
||||
}
|
||||
},
|
||||
"ObservationEvidenceResponse": {
|
||||
"properties": {
|
||||
"memory_id": {
|
||||
"type": "string",
|
||||
"title": "Memory Id",
|
||||
"description": "ID of the memory unit this evidence comes from"
|
||||
},
|
||||
"quote": {
|
||||
"type": "string",
|
||||
"title": "Quote",
|
||||
"description": "Exact quote from the memory supporting the observation"
|
||||
},
|
||||
"relevance": {
|
||||
"type": "string",
|
||||
"title": "Relevance",
|
||||
"description": "Brief explanation of how this quote supports the observation"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"title": "Timestamp",
|
||||
"description": "When the source memory was created (ISO format)"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"memory_id",
|
||||
"quote",
|
||||
"relevance",
|
||||
"timestamp"
|
||||
],
|
||||
"title": "ObservationEvidenceResponse",
|
||||
"description": "A single piece of evidence supporting an observation."
|
||||
},
|
||||
"ObservationInput": {
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Title",
|
||||
"description": "Short title/header for the observation"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"title": "Content",
|
||||
"description": "Content of the observation"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"title",
|
||||
"content"
|
||||
],
|
||||
"title": "ObservationInput",
|
||||
"description": "Input model for a single observation."
|
||||
},
|
||||
"OperationResponse": {
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -5113,22 +4692,24 @@
|
||||
"subtype": {
|
||||
"type": "string",
|
||||
"title": "Subtype",
|
||||
"description": "Mental model subtype: structural, emergent, learned, directive"
|
||||
"description": "Mental model subtype: structural, emergent, learned"
|
||||
},
|
||||
"observations": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"title": "Description",
|
||||
"description": "Brief description"
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Observations",
|
||||
"description": "Observations for directive mental models (subtype='directive')"
|
||||
"title": "Summary",
|
||||
"description": "Full summary (when looked up in detail)"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -5136,7 +4717,8 @@
|
||||
"id",
|
||||
"name",
|
||||
"type",
|
||||
"subtype"
|
||||
"subtype",
|
||||
"description"
|
||||
],
|
||||
"title": "ReflectMentalModel",
|
||||
"description": "A mental model accessed during reflect."
|
||||
@@ -5446,14 +5028,6 @@
|
||||
"type": "array",
|
||||
"title": "Llm Calls",
|
||||
"description": "LLM calls made during reflection"
|
||||
},
|
||||
"mental_models": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ReflectMentalModel"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Mental Models",
|
||||
"description": "Mental models used during reflection (includes directives with subtype='directive')"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -5704,41 +5278,6 @@
|
||||
"title": "UpdateDispositionRequest",
|
||||
"description": "Request model for updating disposition traits."
|
||||
},
|
||||
"UpdateMentalModelRequest": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Name",
|
||||
"description": "New name for the mental model"
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Description",
|
||||
"description": "New description/rule text"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "UpdateMentalModelRequest",
|
||||
"description": "Request model for updating a mental model.",
|
||||
"example": {
|
||||
"description": "Updated description with new rules",
|
||||
"name": "Updated Name"
|
||||
}
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
|
||||
@@ -353,8 +353,8 @@ def _do_configure_interactive():
|
||||
print(f" \033[2mConfig:\033[0m {CONFIG_FILE}")
|
||||
print()
|
||||
print(" \033[2mTest with:\033[0m")
|
||||
print(' \033[36mhindsight-embed retain "Alice works at Google as a software engineer"\033[0m')
|
||||
print(' \033[36mhindsight-embed recall "Alice"\033[0m')
|
||||
print(' \033[36mhindsight-embed retain "Test memory"\033[0m')
|
||||
print(' \033[36mhindsight-embed recall "test"\033[0m')
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
@@ -9,10 +9,6 @@ requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langgraph>=0.2.0",
|
||||
"hindsight-client",
|
||||
# Transitive dependency security fixes
|
||||
"aiohttp>=3.13.3", # Multiple DoS vulnerabilities
|
||||
"langchain-core>=1.2.5", # Serialization injection vulnerability
|
||||
"urllib3>=2.6.3", # Decompression-bomb safeguards bypass
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
|
||||
Generated
+111
-148
@@ -13,7 +13,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.13.3"
|
||||
version = "3.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -25,110 +25,110 @@ dependencies = [
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/34/939730e66b716b76046dedfe0842995842fa906ccc4964bba414ff69e429/aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", size = 736471 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/cf/dcbdf2df7f6ca72b0bb4c0b4509701f2d8942cf54e29ca197389c214c07f/aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", size = 493985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/87/71c8867e0a1d0882dcbc94af767784c3cb381c1c4db0943ab4aae4fed65e/aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", size = 489274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0f/46c24e8dae237295eaadd113edd56dee96ef6462adf19b88592d44891dc5/aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", size = 1668171 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/c6/4cdfb4440d0e28483681a48f69841fa5e39366347d66ef808cbdadddb20e/aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", size = 1636036 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/37/8708cf678628216fb678ab327a4e1711c576d6673998f4f43e86e9ae90dd/aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", size = 1727975 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/2e/3ebfe12fdcb9b5f66e8a0a42dffcd7636844c8a018f261efb2419f68220b/aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", size = 1815823 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/4f/ca2ef819488cbb41844c6cf92ca6dd15b9441e6207c58e5ae0e0fc8d70ad/aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", size = 1669374 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/fe/1fe2e1179a0d91ce09c99069684aab619bf2ccde9b20bd6ca44f8837203e/aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", size = 1555315 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/2b/f3781899b81c45d7cbc7140cddb8a3481c195e7cbff8e36374759d2ab5a5/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", size = 1639140 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/27/c37e85cd3ece6f6c772e549bd5a253d0c122557b25855fb274224811e4f2/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", size = 1645496 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/20/3af1ab663151bd3780b123e907761cdb86ec2c4e44b2d9b195ebc91fbe37/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", size = 1697625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/eb/ae5cab15efa365e13d56b31b0d085a62600298bf398a7986f8388f73b598/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", size = 1542025 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/2d/1683e8d67ec72d911397fe4e575688d2a9b8f6a6e03c8fdc9f3fd3d4c03f/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", size = 1714918 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/a2/ffe8e0e1c57c5e542d47ffa1fcf95ef2b3ea573bf7c4d2ee877252431efc/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", size = 1656113 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/42/d511aff5c3a2b06c09d7d214f508a4ad8ac7799817f7c3d23e7336b5e896/aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", size = 432290 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ea/1c2eb7098b5bad4532994f2b7a8228d27674035c9b3234fe02c37469ef14/aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", size = 455075 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -449,7 +449,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-client"
|
||||
version = "0.3.0"
|
||||
version = "0.0.7"
|
||||
source = { directory = "../../hindsight-clients/python" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -468,7 +468,6 @@ requires-dist = [
|
||||
{ name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.21.0" },
|
||||
{ name = "python-dateutil", specifier = ">=2.8.2" },
|
||||
{ name = "requests", marker = "extra == 'test'", specifier = ">=2.28.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.7.1" },
|
||||
{ name = "urllib3", specifier = ">=2.1.0,<3.0.0" },
|
||||
]
|
||||
@@ -479,11 +478,8 @@ name = "hindsight-langmem"
|
||||
version = "0.0.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "hindsight-client" },
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langgraph" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -493,12 +489,9 @@ test = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiohttp", specifier = ">=3.13.3" },
|
||||
{ name = "hindsight-client", directory = "../../hindsight-clients/python" },
|
||||
{ name = "langchain-core", specifier = ">=1.2.5" },
|
||||
{ name = "langgraph", specifier = ">=0.2.0" },
|
||||
{ name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" },
|
||||
{ name = "urllib3", specifier = ">=2.6.3" },
|
||||
]
|
||||
provides-extras = ["test"]
|
||||
|
||||
@@ -571,7 +564,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.2.7"
|
||||
version = "1.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -581,11 +574,10 @@ dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "tenacity" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "uuid-utils" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/664d8d81b3493e09cbab72448d2f9d693d1fa5aa2bcc488602203a9b6da0/langchain_core-1.2.7.tar.gz", hash = "sha256:e1460639f96c352b4a41c375f25aeb8d16ffc1769499fb1c20503aad59305ced", size = 837039 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/61/c356e19525a210baf960968dbfb03ee38a05e05ddb41efeb32abfcb4e360/langchain_core-1.0.5.tar.gz", hash = "sha256:7ecbad9a60dde626252733a9c18c7377f4468cfe00465ffa99f5e9c6cb9b82d2", size = 778259 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl", hash = "sha256:452f4fef7a3d883357b22600788d37e3d8854ef29da345b7ac7099f33c31828b", size = 490232 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ee/aaf2343a35080154c82ceb110e03dd00f15459bc72e518df51724cbc41a9/langchain_core-1.0.5-py3-none-any.whl", hash = "sha256:d24c0cf12cfcd96dd4bd479aa91425f3a6652226cd824228ae422a195067b74e", size = 471506 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1430,40 +1422,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid-utils"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/8a/17b11768dcb473d3a255c02ffdd94fbd1b345c906efea0a39124dcbaed52/uuid_utils-0.13.0.tar.gz", hash = "sha256:4c17df6427a9e23a4cd7fb9ee1efb53b8abb078660b9bdb2524ca8595022dfe1", size = 21921 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/b8/d40848ca22781f206c60a1885fc737d2640392bd6b5792d455525accd89c/uuid_utils-0.13.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:83628283e977fb212e756bc055df8fdd2f9f589a2e539ba1abe755b8ce8df7a4", size = 602130 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/b9/00a944b8096632ea12638181f8e294abcde3e3b8b5e29b777f809896f6ae/uuid_utils-0.13.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c47638ed6334ab19d80f73664f153b04bbb04ab8ce4298d10da6a292d4d21c47", size = 304213 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/d7/07b36c33aef683b81c9afff3ec178d5eb39d325447a68c3c68a62e4abb32/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:b276b538c57733ed406948584912da422a604313c71479654848b84b9e19c9b0", size = 340624 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/55/fcff2fff02a27866cb1a6614c9df2b3ace721f0a0aab2b7b8f5a7d4e4221/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_armv7l.whl", hash = "sha256:bdaf2b77e34b199cf04cde28399495fd1ed951de214a4ece1f3919b2f945bb06", size = 346705 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/48/67438506c2bb8bee1b4b00d7c0b3ff866401b4790849bf591d654d4ea0bc/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_i686.whl", hash = "sha256:eb2f0baf81e82f9769a7684022dca8f3bf801ca1574a3e94df1876e9d6f9271e", size = 366023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/d7/2d91ce17f62fd764d593430de296b70843cc25229c772453f7261de9e6a8/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_ppc64le.whl", hash = "sha256:6be6c4d11275f5cc402a4fdba6c2b1ce45fd3d99bb78716cd1cc2cbf6802b2ce", size = 471149 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/9a/aa0756186073ba84daf5704c150d41ede10eb3185d510e02532e2071550e/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:77621cf6ceca7f42173a642a01c01c216f9eaec3b7b65d093d2d6a433ca0a83d", size = 342130 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/b4/3191789f4dc3bed59d79cec90559821756297a25d7dc34d1bf7781577a75/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a5a9eb06c2bb86dd876cd7b2fe927fc8543d14c90d971581db6ffda4a02526f", size = 524128 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/30/29839210a8fff9fc219bfa7c8d8cd115324e92618cba0cda090d54d3d321/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:775347c6110fb71360df17aac74132d8d47c1dbe71233ac98197fc872a791fd2", size = 615872 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/ed/15000c96a8bd8f5fd8efd622109bf52549ea0b366f8ce71c45580fa55878/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf95f6370ad1a0910ee7b5ad5228fd19c4ae32fe3627389006adaf519408c41e", size = 581023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/c8/3f809fa2dc2ca4bd331c792a3c7d3e45ae2b709d85847a12b8b27d1d5f19/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a88e23e0b2f4203fefe2ccbca5736ee06fcad10e61b5e7e39c8d7904bc13300", size = 546715 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/80/4f7c7efd734d1494397c781bd3d421688e9c187ae836e3174625b1ddf8b0/uuid_utils-0.13.0-cp39-abi3-win32.whl", hash = "sha256:3e4f2cc54e6a99c0551158100ead528479ad2596847478cbad624977064ffce3", size = 177650 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/94/d05ab68622e66ad787a241dfe5ccc649b3af09f30eae977b9ee8f7046aaa/uuid_utils-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:046cb2756e1597b3de22d24851b769913e192135830486a0a70bf41327f0360c", size = 183211 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/37/674b3ce25cd715b831ea8ebbd828b74c40159f04c95d1bb963b2c876fe79/uuid_utils-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:5447a680df6ef8a5a353976aaf4c97cc3a3a22b1ee13671c44227b921e3ae2a9", size = 183518 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/fa/1d92de9538463859228e68db679b766fd300770c9a2db849dcba0c0c5a57/uuid_utils-0.13.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e5182e2d95f38e65f2e5bce90648ef56987443da13e145afcd747e584f9bc69c", size = 587641 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/07/6bd9e6f5367e38c2ee7178ad882d2bd1b0d17c5393974b09ab027a215eba/uuid_utils-0.13.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e3909a8a1fbd79d7c8bdc874eeb83e23ccb7a7cb0aa821a49596cc96c0cce84b", size = 298273 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/14/7061b868a8a6799c8df83768a23f313d4e22075069f01ee3c28fa82aa2c6/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:5dc4c9f749bd2511b8dcbf0891e658d7d86880022963db050722ad7b502b5e22", size = 333618 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f1/f48c3c9c343c9071ade5f355403e344d817412d9cf379a2d04b181282e74/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_armv7l.whl", hash = "sha256:516adf07f5b2cdb88d50f489c702b5f1a75ae8b2639bfd254f4192d5f7ee261f", size = 339104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/22/8e3142b4baffee77ce533fe956446d3699ec42f1d5252911208cbef4501e/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_i686.whl", hash = "sha256:aeee3bd89e8de6184a3ab778ce19f5ce9ad32849d1be549516e0ddb257562d8d", size = 359503 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/1a/756f1f9e31b15019c87cd2becb1c596351c50967cd143443da38df8818d1/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_ppc64le.whl", hash = "sha256:97985256c2e59b7caa51f5c8515f64d777328562a9c900ec65e9d627baf72737", size = 467480 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/20/a6929e98d9a461ca49e96194a82a1cc3fd5420f3a2f53cbb34fca438549e/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:b7ccaa20e24c5f60f41a69ef571ed820737f9b0ade4cbeef56aaa8f80f5aa475", size = 333610 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -33,10 +33,6 @@ classifiers = [
|
||||
|
||||
dependencies = [
|
||||
"litellm>=1.40.0",
|
||||
# Transitive dependency security fixes
|
||||
"aiohttp>=3.13.3", # Multiple DoS vulnerabilities
|
||||
"filelock>=3.20.3", # TOCTOU race condition
|
||||
"urllib3>=2.6.3", # Decompression-bomb safeguards bypass
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
Generated
+111
-117
@@ -17,7 +17,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.13.3"
|
||||
version = "3.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -29,110 +29,110 @@ dependencies = [
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/34/939730e66b716b76046dedfe0842995842fa906ccc4964bba414ff69e429/aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", size = 736471 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/cf/dcbdf2df7f6ca72b0bb4c0b4509701f2d8942cf54e29ca197389c214c07f/aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", size = 493985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/87/71c8867e0a1d0882dcbc94af767784c3cb381c1c4db0943ab4aae4fed65e/aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", size = 489274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0f/46c24e8dae237295eaadd113edd56dee96ef6462adf19b88592d44891dc5/aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", size = 1668171 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/c6/4cdfb4440d0e28483681a48f69841fa5e39366347d66ef808cbdadddb20e/aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", size = 1636036 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/37/8708cf678628216fb678ab327a4e1711c576d6673998f4f43e86e9ae90dd/aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", size = 1727975 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/2e/3ebfe12fdcb9b5f66e8a0a42dffcd7636844c8a018f261efb2419f68220b/aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", size = 1815823 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/4f/ca2ef819488cbb41844c6cf92ca6dd15b9441e6207c58e5ae0e0fc8d70ad/aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", size = 1669374 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/fe/1fe2e1179a0d91ce09c99069684aab619bf2ccde9b20bd6ca44f8837203e/aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", size = 1555315 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/2b/f3781899b81c45d7cbc7140cddb8a3481c195e7cbff8e36374759d2ab5a5/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", size = 1639140 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/27/c37e85cd3ece6f6c772e549bd5a253d0c122557b25855fb274224811e4f2/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", size = 1645496 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/20/3af1ab663151bd3780b123e907761cdb86ec2c4e44b2d9b195ebc91fbe37/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", size = 1697625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/eb/ae5cab15efa365e13d56b31b0d085a62600298bf398a7986f8388f73b598/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", size = 1542025 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/2d/1683e8d67ec72d911397fe4e575688d2a9b8f6a6e03c8fdc9f3fd3d4c03f/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", size = 1714918 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/a2/ffe8e0e1c57c5e542d47ffa1fcf95ef2b3ea573bf7c4d2ee877252431efc/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", size = 1656113 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/42/d511aff5c3a2b06c09d7d214f508a4ad8ac7799817f7c3d23e7336b5e896/aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", size = 432290 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ea/1c2eb7098b5bad4532994f2b7a8228d27674035c9b3234fe02c37469ef14/aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", size = 455075 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -403,11 +403,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.3"
|
||||
version = "3.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -691,13 +691,10 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-litellm"
|
||||
version = "0.3.0"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "filelock" },
|
||||
{ name = "litellm" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -709,13 +706,10 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiohttp", specifier = ">=3.13.3" },
|
||||
{ name = "filelock", specifier = ">=3.20.3" },
|
||||
{ name = "litellm", specifier = ">=1.40.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
|
||||
{ name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.10.0" },
|
||||
{ name = "urllib3", specifier = ">=2.6.3" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
@@ -2051,11 +2045,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
version = "2.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -7,9 +7,6 @@ requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"openai>=1.0.0",
|
||||
"hindsight-client",
|
||||
# Transitive dependency security fixes
|
||||
"aiohttp>=3.13.3", # Multiple DoS vulnerabilities
|
||||
"urllib3>=2.6.3", # Decompression-bomb safeguards bypass
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
|
||||
Generated
+108
-113
@@ -13,7 +13,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.13.3"
|
||||
version = "3.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -25,110 +25,110 @@ dependencies = [
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/34/939730e66b716b76046dedfe0842995842fa906ccc4964bba414ff69e429/aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", size = 736471 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/cf/dcbdf2df7f6ca72b0bb4c0b4509701f2d8942cf54e29ca197389c214c07f/aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", size = 493985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/87/71c8867e0a1d0882dcbc94af767784c3cb381c1c4db0943ab4aae4fed65e/aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", size = 489274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0f/46c24e8dae237295eaadd113edd56dee96ef6462adf19b88592d44891dc5/aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", size = 1668171 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/c6/4cdfb4440d0e28483681a48f69841fa5e39366347d66ef808cbdadddb20e/aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", size = 1636036 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/37/8708cf678628216fb678ab327a4e1711c576d6673998f4f43e86e9ae90dd/aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", size = 1727975 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/2e/3ebfe12fdcb9b5f66e8a0a42dffcd7636844c8a018f261efb2419f68220b/aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", size = 1815823 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/4f/ca2ef819488cbb41844c6cf92ca6dd15b9441e6207c58e5ae0e0fc8d70ad/aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", size = 1669374 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/fe/1fe2e1179a0d91ce09c99069684aab619bf2ccde9b20bd6ca44f8837203e/aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", size = 1555315 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/2b/f3781899b81c45d7cbc7140cddb8a3481c195e7cbff8e36374759d2ab5a5/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", size = 1639140 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/27/c37e85cd3ece6f6c772e549bd5a253d0c122557b25855fb274224811e4f2/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", size = 1645496 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/20/3af1ab663151bd3780b123e907761cdb86ec2c4e44b2d9b195ebc91fbe37/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", size = 1697625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/eb/ae5cab15efa365e13d56b31b0d085a62600298bf398a7986f8388f73b598/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", size = 1542025 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/2d/1683e8d67ec72d911397fe4e575688d2a9b8f6a6e03c8fdc9f3fd3d4c03f/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", size = 1714918 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/a2/ffe8e0e1c57c5e542d47ffa1fcf95ef2b3ea573bf7c4d2ee877252431efc/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", size = 1656113 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/42/d511aff5c3a2b06c09d7d214f508a4ad8ac7799817f7c3d23e7336b5e896/aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", size = 432290 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ea/1c2eb7098b5bad4532994f2b7a8228d27674035c9b3234fe02c37469ef14/aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", size = 455075 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -378,7 +378,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-client"
|
||||
version = "0.3.0"
|
||||
version = "0.0.7"
|
||||
source = { directory = "../../hindsight-clients/python" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -397,7 +397,6 @@ requires-dist = [
|
||||
{ name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.21.0" },
|
||||
{ name = "python-dateutil", specifier = ">=2.8.2" },
|
||||
{ name = "requests", marker = "extra == 'test'", specifier = ">=2.28.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.7.1" },
|
||||
{ name = "urllib3", specifier = ">=2.1.0,<3.0.0" },
|
||||
]
|
||||
@@ -408,10 +407,8 @@ name = "hindsight-openai"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "hindsight-client" },
|
||||
{ name = "openai" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -423,13 +420,11 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiohttp", specifier = ">=3.13.3" },
|
||||
{ name = "hindsight-client", directory = "../../hindsight-clients/python" },
|
||||
{ name = "openai", specifier = ">=1.0.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
|
||||
{ name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.10.0" },
|
||||
{ name = "urllib3", specifier = ">=2.6.3" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
@@ -1165,11 +1160,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Generated
-84
@@ -145,8 +145,6 @@
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
@@ -7590,88 +7588,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
|
||||
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
|
||||
"integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-visually-hidden": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
|
||||
Reference in New Issue
Block a user