rename to new names

This commit is contained in:
Nicolò Boschi 2025-11-27 16:22:16 +01:00
parent a3ad76d165
commit d099ef870d
1939 changed files with 28551 additions and 27114 deletions

View file

@ -34,12 +34,15 @@ from hindsight import HindsightServer, HindsightClient
with HindsightServer(llm_provider="openai", llm_model="gpt-5.1-mini", llm_api_key=os.environ["OPENAI_API_KEY"]) as server:
client = HindsightClient(base_url=server.url)
client.put(agent_id="my-agent", content="Alice works at Google")
client.put(agent_id="my-agent", content="Bob prefers Python over JavaScript")
# Retain memories
client.retain(bank_id="my-agent", content="Alice works at Google")
client.retain(bank_id="my-agent", content="Bob prefers Python over JavaScript")
client.search(agent_id="my-agent", query="What does Alice do?")
# Recall memories
client.recall(bank_id="my-agent", query="What does Alice do?")
client.think(agent_id="my-agent", query="Tell me about Alice")
# Get memory perspective
client.reflect(bank_id="my-agent", query="Tell me about Alice")
```

View file

@ -1,40 +0,0 @@
"""Fix memory_links entity_id to be nullable
Revision ID: 01f989db9079
Revises: af0413383b3e
Create Date: 2025-11-03 14:43:18.721430
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '01f989db9079'
down_revision: Union[str, Sequence[str], None] = 'af0413383b3e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Drop the existing primary key
op.execute('ALTER TABLE memory_links DROP CONSTRAINT memory_links_pkey')
# Change entity_id to nullable
op.alter_column('memory_links', 'entity_id',
existing_type=sa.UUID(),
nullable=True)
# Create a unique index with COALESCE expression to handle NULL entity_id
op.execute("""
CREATE UNIQUE INDEX idx_memory_links_unique
ON memory_links (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid))
""")
def downgrade() -> None:
"""Downgrade schema."""
pass

View file

@ -1,48 +0,0 @@
"""add_async_operations_table
Revision ID: 0e96398aae9e
Revises: 1a35a4fa1950
Create Date: 2025-11-07 14:54:21.224968
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '0e96398aae9e'
down_revision: Union[str, Sequence[str], None] = '1a35a4fa1950'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Create async_operations table
op.execute("""
CREATE TABLE async_operations (
id UUID PRIMARY KEY,
agent_id TEXT NOT NULL,
task_type TEXT NOT NULL,
items_count INTEGER NOT NULL,
document_id TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
)
""")
# Create index on agent_id for fast lookups by agent
op.execute("""
CREATE INDEX idx_async_operations_agent_id
ON async_operations(agent_id)
""")
def downgrade() -> None:
"""Downgrade schema."""
# Drop index
op.execute("DROP INDEX IF EXISTS idx_async_operations_agent_id")
# Drop table
op.execute("DROP TABLE IF EXISTS async_operations")

View file

@ -1,47 +0,0 @@
"""add_agents_table
Revision ID: 1680fc9768b4
Revises: 8c55f5602451
Create Date: 2025-11-12 16:18:06.620862
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '1680fc9768b4'
down_revision: Union[str, Sequence[str], None] = '8c55f5602451'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Create agents table
op.execute("""
CREATE TABLE agents (
agent_id TEXT PRIMARY KEY,
personality JSONB NOT NULL DEFAULT '{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, "agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}'::jsonb,
background TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
)
""")
# Create index on agent_id for fast lookups
op.execute("""
CREATE INDEX idx_agents_agent_id
ON agents(agent_id)
""")
def downgrade() -> None:
"""Downgrade schema."""
# Drop index
op.execute("DROP INDEX IF EXISTS idx_agents_agent_id")
# Drop table
op.execute("DROP TABLE IF EXISTS agents")

View file

@ -1,74 +0,0 @@
"""add_bm25_fulltext_search
Revision ID: 1a35a4fa1950
Revises: 01f989db9079
Create Date: 2025-11-06 11:19:48.627698
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '1a35a4fa1950'
down_revision: Union[str, Sequence[str], None] = '01f989db9079'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Add tsvector column for full-text search
op.execute("""
ALTER TABLE memory_units
ADD COLUMN search_vector tsvector
""")
# Populate tsvector with existing data (text + context combined)
op.execute("""
UPDATE memory_units
SET search_vector =
setweight(to_tsvector('english', COALESCE(text, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(context, '')), 'B')
""")
# Create GIN index for fast full-text search
op.execute("""
CREATE INDEX idx_memory_units_search_vector
ON memory_units
USING GIN(search_vector)
""")
# Create trigger to auto-update tsvector on INSERT/UPDATE
op.execute("""
CREATE OR REPLACE FUNCTION memory_units_search_vector_trigger() RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', COALESCE(NEW.text, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.context, '')), 'B');
RETURN NEW;
END
$$ LANGUAGE plpgsql;
""")
op.execute("""
CREATE TRIGGER update_memory_units_search_vector
BEFORE INSERT OR UPDATE ON memory_units
FOR EACH ROW
EXECUTE FUNCTION memory_units_search_vector_trigger();
""")
def downgrade() -> None:
"""Downgrade schema."""
# Drop trigger
op.execute("DROP TRIGGER IF EXISTS update_memory_units_search_vector ON memory_units")
op.execute("DROP FUNCTION IF EXISTS memory_units_search_vector_trigger()")
# Drop index
op.execute("DROP INDEX IF EXISTS idx_memory_units_search_vector")
# Drop column
op.execute("ALTER TABLE memory_units DROP COLUMN IF EXISTS search_vector")

View file

@ -1,28 +0,0 @@
"""merge agents and temporal ranges branches
Revision ID: 217b2227771f
Revises: 3b9c4d8e7f21, 9d42e6f91234
Create Date: 2025-11-17 14:59:01.254543
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '217b2227771f'
down_revision: Union[str, Sequence[str], None] = ('3b9c4d8e7f21', '9d42e6f91234')
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View file

@ -1,49 +0,0 @@
"""add_status_and_error_to_async_operations
Revision ID: 2a76a5bc2f09
Revises: 0e96398aae9e
Create Date: 2025-11-07 16:03:19.078561
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '2a76a5bc2f09'
down_revision: Union[str, Sequence[str], None] = '0e96398aae9e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Add status column (default 'pending' for existing rows)
op.execute("""
ALTER TABLE async_operations
ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'
""")
# Add error_message column
op.execute("""
ALTER TABLE async_operations
ADD COLUMN error_message TEXT
""")
# Add index on status for filtering failed/pending operations
op.execute("""
CREATE INDEX idx_async_operations_status
ON async_operations(status)
""")
def downgrade() -> None:
"""Downgrade schema."""
# Drop index
op.execute("DROP INDEX IF EXISTS idx_async_operations_status")
# Drop columns
op.execute("ALTER TABLE async_operations DROP COLUMN IF EXISTS error_message")
op.execute("ALTER TABLE async_operations DROP COLUMN IF EXISTS status")

View file

@ -1,36 +0,0 @@
"""add_name_to_agents
Revision ID: 3b9c4d8e7f21
Revises: 1680fc9768b4
Create Date: 2025-11-13 14:52:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '3b9c4d8e7f21'
down_revision: Union[str, Sequence[str], None] = '1680fc9768b4'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Add name column to agents table
op.execute("""
ALTER TABLE agents
ADD COLUMN name TEXT NOT NULL DEFAULT ''
""")
def downgrade() -> None:
"""Downgrade schema."""
# Remove name column from agents table
op.execute("""
ALTER TABLE agents
DROP COLUMN name
""")

View file

@ -1,32 +0,0 @@
"""add metadata to memory_units
Revision ID: 4a8b3c5d6e7f
Revises: 217b2227771f
Create Date: 2025-11-21 10:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '4a8b3c5d6e7f'
down_revision: Union[str, Sequence[str], None] = '217b2227771f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Add metadata column to memory_units table."""
op.add_column(
'memory_units',
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False)
)
def downgrade() -> None:
"""Remove metadata column from memory_units table."""
op.drop_column('memory_units', 'metadata')

View file

@ -0,0 +1,275 @@
"""initial_schema
Revision ID: 5a366d414dce
Revises:
Create Date: 2025-11-27 11:54:19.228030
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from pgvector.sqlalchemy import Vector
# revision identifiers, used by Alembic.
revision: str = '5a366d414dce'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema - create all tables from scratch."""
# Enable required extensions
op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
op.execute('CREATE EXTENSION IF NOT EXISTS vector')
# Create banks table
op.create_table(
'banks',
sa.Column('bank_id', sa.Text(), nullable=False),
sa.Column('name', sa.Text(), nullable=True),
sa.Column('personality', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
sa.Column('background', sa.Text(), nullable=True),
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('bank_id', name=op.f('pk_banks'))
)
# Create documents table
op.create_table(
'documents',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('bank_id', sa.Text(), nullable=False),
sa.Column('original_text', sa.Text(), nullable=True),
sa.Column('content_hash', sa.Text(), nullable=True),
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id', 'bank_id', name=op.f('pk_documents'))
)
op.create_index('idx_documents_bank_id', 'documents', ['bank_id'])
op.create_index('idx_documents_content_hash', 'documents', ['content_hash'])
# Create async_operations table
op.create_table(
'async_operations',
sa.Column('operation_id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('bank_id', sa.Text(), nullable=False),
sa.Column('operation_type', sa.Text(), nullable=False),
sa.Column('status', sa.Text(), server_default='pending', nullable=False),
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('completed_at', postgresql.TIMESTAMP(timezone=True), nullable=True),
sa.Column('error_message', sa.Text(), nullable=True),
sa.Column('result_metadata', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
sa.PrimaryKeyConstraint('operation_id', name=op.f('pk_async_operations')),
sa.CheckConstraint("status IN ('pending', 'processing', 'completed', 'failed')", name='async_operations_status_check')
)
op.create_index('idx_async_operations_bank_id', 'async_operations', ['bank_id'])
op.create_index('idx_async_operations_status', 'async_operations', ['status'])
op.create_index('idx_async_operations_bank_status', 'async_operations', ['bank_id', 'status'])
# Create entities table
op.create_table(
'entities',
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('canonical_name', sa.Text(), nullable=False),
sa.Column('bank_id', sa.Text(), nullable=False),
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
sa.Column('first_seen', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('last_seen', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('mention_count', sa.Integer(), server_default='1', nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_entities'))
)
op.create_index('idx_entities_bank_id', 'entities', ['bank_id'])
op.create_index('idx_entities_canonical_name', 'entities', ['canonical_name'])
op.create_index('idx_entities_bank_name', 'entities', ['bank_id', 'canonical_name'])
# Create unique index on (bank_id, LOWER(canonical_name)) for entity resolution
op.execute('CREATE UNIQUE INDEX idx_entities_bank_lower_name ON entities (bank_id, LOWER(canonical_name))')
# Create memory_units table
op.create_table(
'memory_units',
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('bank_id', sa.Text(), nullable=False),
sa.Column('document_id', sa.Text(), nullable=True),
sa.Column('text', sa.Text(), nullable=False),
sa.Column('embedding', Vector(384), nullable=True),
sa.Column('context', sa.Text(), nullable=True),
sa.Column('event_date', postgresql.TIMESTAMP(timezone=True), nullable=False),
sa.Column('occurred_start', postgresql.TIMESTAMP(timezone=True), nullable=True),
sa.Column('occurred_end', postgresql.TIMESTAMP(timezone=True), nullable=True),
sa.Column('mentioned_at', postgresql.TIMESTAMP(timezone=True), nullable=True),
sa.Column('fact_type', sa.Text(), server_default='world', nullable=False),
sa.Column('confidence_score', sa.Float(), nullable=True),
sa.Column('access_count', sa.Integer(), server_default='0', nullable=False),
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), nullable=False),
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['document_id', 'bank_id'], ['documents.id', 'documents.bank_id'], name='memory_units_document_fkey', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_memory_units')),
sa.CheckConstraint("fact_type IN ('world', 'bank', 'opinion', 'observation')", name='memory_units_fact_type_check'),
sa.CheckConstraint("confidence_score IS NULL OR (confidence_score >= 0.0 AND confidence_score <= 1.0)", name='memory_units_confidence_range_check'),
sa.CheckConstraint(
"(fact_type = 'opinion' AND confidence_score IS NOT NULL) OR "
"(fact_type = 'observation') OR "
"(fact_type NOT IN ('opinion', 'observation') AND confidence_score IS NULL)",
name='confidence_score_fact_type_check'
)
)
# Add search_vector column for full-text search
op.execute("""
ALTER TABLE memory_units
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('english', COALESCE(text, '') || ' ' || COALESCE(context, ''))) STORED
""")
op.create_index('idx_memory_units_bank_id', 'memory_units', ['bank_id'])
op.create_index('idx_memory_units_document_id', 'memory_units', ['document_id'])
op.create_index('idx_memory_units_event_date', 'memory_units', [sa.text('event_date DESC')])
op.create_index('idx_memory_units_bank_date', 'memory_units', ['bank_id', sa.text('event_date DESC')])
op.create_index('idx_memory_units_access_count', 'memory_units', [sa.text('access_count DESC')])
op.create_index('idx_memory_units_fact_type', 'memory_units', ['fact_type'])
op.create_index('idx_memory_units_bank_fact_type', 'memory_units', ['bank_id', 'fact_type'])
op.create_index('idx_memory_units_bank_type_date', 'memory_units', ['bank_id', 'fact_type', sa.text('event_date DESC')])
op.create_index('idx_memory_units_opinion_confidence', 'memory_units', ['bank_id', sa.text('confidence_score DESC')], postgresql_where=sa.text("fact_type = 'opinion'"))
op.create_index('idx_memory_units_opinion_date', 'memory_units', ['bank_id', sa.text('event_date DESC')], postgresql_where=sa.text("fact_type = 'opinion'"))
op.create_index('idx_memory_units_observation_date', 'memory_units', ['bank_id', sa.text('event_date DESC')], postgresql_where=sa.text("fact_type = 'observation'"))
op.create_index('idx_memory_units_embedding', 'memory_units', ['embedding'], postgresql_using='hnsw', postgresql_ops={'embedding': 'vector_cosine_ops'})
# Create BM25 full-text search index on search_vector
op.execute("""
CREATE INDEX idx_memory_units_text_search ON memory_units
USING gin(search_vector)
""")
op.execute("""
CREATE MATERIALIZED VIEW memory_units_bm25 AS
SELECT
id,
bank_id,
text,
to_tsvector('english', text) AS text_vector,
log(1.0 + length(text)::float / (SELECT avg(length(text)) FROM memory_units)) AS doc_length_factor
FROM memory_units
""")
op.create_index('idx_memory_units_bm25_bank', 'memory_units_bm25', ['bank_id'])
op.create_index('idx_memory_units_bm25_text_vector', 'memory_units_bm25', ['text_vector'], postgresql_using='gin')
# Create entity_cooccurrences table
op.create_table(
'entity_cooccurrences',
sa.Column('entity_id_1', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('entity_id_2', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('cooccurrence_count', sa.Integer(), server_default='1', nullable=False),
sa.Column('last_cooccurred', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['entity_id_1'], ['entities.id'], name=op.f('fk_entity_cooccurrences_entity_id_1_entities'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['entity_id_2'], ['entities.id'], name=op.f('fk_entity_cooccurrences_entity_id_2_entities'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('entity_id_1', 'entity_id_2', name=op.f('pk_entity_cooccurrences')),
sa.CheckConstraint('entity_id_1 < entity_id_2', name='entity_cooccurrence_order_check')
)
op.create_index('idx_entity_cooccurrences_entity1', 'entity_cooccurrences', ['entity_id_1'])
op.create_index('idx_entity_cooccurrences_entity2', 'entity_cooccurrences', ['entity_id_2'])
op.create_index('idx_entity_cooccurrences_count', 'entity_cooccurrences', [sa.text('cooccurrence_count DESC')])
# Create memory_links table
op.create_table(
'memory_links',
sa.Column('from_unit_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('to_unit_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('link_type', sa.Text(), nullable=False),
sa.Column('entity_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('weight', sa.Float(), server_default='1.0', nullable=False),
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['entity_id'], ['entities.id'], name=op.f('fk_memory_links_entity_id_entities'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['from_unit_id'], ['memory_units.id'], name=op.f('fk_memory_links_from_unit_id_memory_units'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['to_unit_id'], ['memory_units.id'], name=op.f('fk_memory_links_to_unit_id_memory_units'), ondelete='CASCADE'),
sa.CheckConstraint("link_type IN ('temporal', 'semantic', 'entity', 'causes', 'caused_by', 'enables', 'prevents')", name='memory_links_link_type_check'),
sa.CheckConstraint('weight >= 0.0 AND weight <= 1.0', name='memory_links_weight_check')
)
# Create unique constraint using COALESCE for nullable entity_id
op.execute("CREATE UNIQUE INDEX idx_memory_links_unique ON memory_links (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid))")
op.create_index('idx_memory_links_from_unit', 'memory_links', ['from_unit_id'])
op.create_index('idx_memory_links_to_unit', 'memory_links', ['to_unit_id'])
op.create_index('idx_memory_links_entity', 'memory_links', ['entity_id'])
op.create_index('idx_memory_links_link_type', 'memory_links', ['link_type'])
# Create unit_entities table
op.create_table(
'unit_entities',
sa.Column('unit_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('entity_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['entity_id'], ['entities.id'], name=op.f('fk_unit_entities_entity_id_entities'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['unit_id'], ['memory_units.id'], name=op.f('fk_unit_entities_unit_id_memory_units'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('unit_id', 'entity_id', name=op.f('pk_unit_entities'))
)
op.create_index('idx_unit_entities_unit', 'unit_entities', ['unit_id'])
op.create_index('idx_unit_entities_entity', 'unit_entities', ['entity_id'])
def downgrade() -> None:
"""Downgrade schema - drop all tables."""
# Drop tables in reverse dependency order
op.drop_index('idx_unit_entities_entity', table_name='unit_entities')
op.drop_index('idx_unit_entities_unit', table_name='unit_entities')
op.drop_table('unit_entities')
op.drop_index('idx_memory_links_link_type', table_name='memory_links')
op.drop_index('idx_memory_links_entity', table_name='memory_links')
op.drop_index('idx_memory_links_to_unit', table_name='memory_links')
op.drop_index('idx_memory_links_from_unit', table_name='memory_links')
op.execute('DROP INDEX IF EXISTS idx_memory_links_unique')
op.drop_table('memory_links')
op.drop_index('idx_entity_cooccurrences_count', table_name='entity_cooccurrences')
op.drop_index('idx_entity_cooccurrences_entity2', table_name='entity_cooccurrences')
op.drop_index('idx_entity_cooccurrences_entity1', table_name='entity_cooccurrences')
op.drop_table('entity_cooccurrences')
# Drop BM25 materialized view and index
op.drop_index('idx_memory_units_bm25_text_vector', table_name='memory_units_bm25')
op.drop_index('idx_memory_units_bm25_bank', table_name='memory_units_bm25')
op.execute('DROP MATERIALIZED VIEW IF EXISTS memory_units_bm25')
op.drop_index('idx_memory_units_embedding', table_name='memory_units')
op.drop_index('idx_memory_units_observation_date', table_name='memory_units')
op.drop_index('idx_memory_units_opinion_date', table_name='memory_units')
op.drop_index('idx_memory_units_opinion_confidence', table_name='memory_units')
op.drop_index('idx_memory_units_bank_type_date', table_name='memory_units')
op.drop_index('idx_memory_units_bank_fact_type', table_name='memory_units')
op.drop_index('idx_memory_units_fact_type', table_name='memory_units')
op.drop_index('idx_memory_units_access_count', table_name='memory_units')
op.drop_index('idx_memory_units_bank_date', table_name='memory_units')
op.drop_index('idx_memory_units_event_date', table_name='memory_units')
op.drop_index('idx_memory_units_document_id', table_name='memory_units')
op.drop_index('idx_memory_units_bank_id', table_name='memory_units')
op.execute('DROP INDEX IF EXISTS idx_memory_units_text_search')
op.drop_table('memory_units')
op.execute('DROP INDEX IF EXISTS idx_entities_bank_lower_name')
op.drop_index('idx_entities_bank_name', table_name='entities')
op.drop_index('idx_entities_canonical_name', table_name='entities')
op.drop_index('idx_entities_bank_id', table_name='entities')
op.drop_table('entities')
op.drop_index('idx_async_operations_bank_status', table_name='async_operations')
op.drop_index('idx_async_operations_status', table_name='async_operations')
op.drop_index('idx_async_operations_bank_id', table_name='async_operations')
op.drop_table('async_operations')
op.drop_index('idx_documents_content_hash', table_name='documents')
op.drop_index('idx_documents_bank_id', table_name='documents')
op.drop_table('documents')
op.drop_table('banks')
# Drop extensions (optional - comment out if you want to keep them)
# op.execute('DROP EXTENSION IF EXISTS vector')
# op.execute('DROP EXTENSION IF EXISTS "uuid-ossp"')

View file

@ -1,89 +0,0 @@
"""add_observation_fact_type
Revision ID: 5b2c6d8e9f01
Revises: 4a8b3c5d6e7f
Create Date: 2025-11-26 10:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '5b2c6d8e9f01'
down_revision: Union[str, Sequence[str], None] = '4a8b3c5d6e7f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Drop old constraints
op.execute("""
ALTER TABLE memory_units
DROP CONSTRAINT IF EXISTS confidence_score_fact_type_check
""")
op.execute("""
ALTER TABLE memory_units
DROP CONSTRAINT IF EXISTS memory_units_fact_type_check
""")
# Add new fact_type constraint including 'observation'
op.execute("""
ALTER TABLE memory_units
ADD CONSTRAINT memory_units_fact_type_check
CHECK (fact_type IN ('world', 'agent', 'opinion', 'observation'))
""")
# Add new confidence_score constraint allowing observation to have optional confidence
op.execute("""
ALTER TABLE memory_units
ADD CONSTRAINT confidence_score_fact_type_check
CHECK (
(fact_type = 'opinion' AND confidence_score IS NOT NULL) OR
(fact_type = 'observation') OR
(fact_type NOT IN ('opinion', 'observation') AND confidence_score IS NULL)
)
""")
# Add index for observation fact_type queries
op.execute("""
CREATE INDEX IF NOT EXISTS idx_memory_units_observation_date
ON memory_units (agent_id, event_date DESC)
WHERE fact_type = 'observation'
""")
def downgrade() -> None:
"""Downgrade schema."""
# Drop observation index
op.execute("DROP INDEX IF EXISTS idx_memory_units_observation_date")
# Drop new constraints
op.execute("""
ALTER TABLE memory_units
DROP CONSTRAINT IF EXISTS confidence_score_fact_type_check
""")
op.execute("""
ALTER TABLE memory_units
DROP CONSTRAINT IF EXISTS memory_units_fact_type_check
""")
# Restore old fact_type constraint
op.execute("""
ALTER TABLE memory_units
ADD CONSTRAINT memory_units_fact_type_check
CHECK (fact_type IN ('world', 'agent', 'opinion'))
""")
# Restore old confidence_score constraint
op.execute("""
ALTER TABLE memory_units
ADD CONSTRAINT confidence_score_fact_type_check
CHECK (
(fact_type = 'opinion' AND confidence_score IS NOT NULL) OR
(fact_type != 'opinion' AND confidence_score IS NULL)
)
""")

View file

@ -1,117 +0,0 @@
"""add unique constraint on entities (agent_id, canonical_name)
Revision ID: 7d4e6f0a3b12
Revises: 5b2c6d8e9f01
Create Date: 2024-01-01 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '7d4e6f0a3b12'
down_revision: Union[str, None] = '5b2c6d8e9f01'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# First, deduplicate existing entities by merging duplicates
# Keep the one with highest mention_count, update unit_entities to point to it
op.execute("""
-- Create temp table with canonical entity per (agent_id, name)
CREATE TEMP TABLE canonical_entities AS
SELECT DISTINCT ON (agent_id, LOWER(canonical_name))
id as keep_id,
agent_id,
LOWER(canonical_name) as name_lower
FROM entities
ORDER BY agent_id, LOWER(canonical_name), mention_count DESC, first_seen ASC;
-- Get all entity IDs that will be removed (duplicates)
CREATE TEMP TABLE duplicate_entities AS
SELECT e.id as dup_id, ce.keep_id
FROM entities e
JOIN canonical_entities ce ON e.agent_id = ce.agent_id AND LOWER(e.canonical_name) = ce.name_lower
WHERE e.id != ce.keep_id;
-- Update unit_entities to point to canonical entity
UPDATE unit_entities ue
SET entity_id = de.keep_id
FROM duplicate_entities de
WHERE ue.entity_id = de.dup_id;
-- Delete duplicate unit_entities that now exist
DELETE FROM unit_entities a
USING unit_entities b
WHERE a.unit_id = b.unit_id
AND a.entity_id = b.entity_id
AND a.ctid < b.ctid;
-- For entity_cooccurrences, we need to be careful about the check constraint
-- First, collect all cooccurrences that need updating into a temp table with correct ordering
CREATE TEMP TABLE new_cooccurrences AS
SELECT DISTINCT
LEAST(
COALESCE(de1.keep_id, ec.entity_id_1),
COALESCE(de2.keep_id, ec.entity_id_2)
) as entity_id_1,
GREATEST(
COALESCE(de1.keep_id, ec.entity_id_1),
COALESCE(de2.keep_id, ec.entity_id_2)
) as entity_id_2,
SUM(ec.cooccurrence_count) as cooccurrence_count,
MAX(ec.last_cooccurred) as last_cooccurred
FROM entity_cooccurrences ec
LEFT JOIN duplicate_entities de1 ON ec.entity_id_1 = de1.dup_id
LEFT JOIN duplicate_entities de2 ON ec.entity_id_2 = de2.dup_id
GROUP BY
LEAST(COALESCE(de1.keep_id, ec.entity_id_1), COALESCE(de2.keep_id, ec.entity_id_2)),
GREATEST(COALESCE(de1.keep_id, ec.entity_id_1), COALESCE(de2.keep_id, ec.entity_id_2));
-- Delete rows where entity_id_1 = entity_id_2 (self-references after merge)
DELETE FROM new_cooccurrences WHERE entity_id_1 = entity_id_2;
-- Delete all old cooccurrences
DELETE FROM entity_cooccurrences;
-- Insert the merged cooccurrences
INSERT INTO entity_cooccurrences (entity_id_1, entity_id_2, cooccurrence_count, last_cooccurred)
SELECT entity_id_1, entity_id_2, cooccurrence_count, last_cooccurred
FROM new_cooccurrences;
-- Update mention counts on canonical entities
UPDATE entities e
SET mention_count = e.mention_count + COALESCE(
(SELECT SUM(e2.mention_count)
FROM entities e2
JOIN duplicate_entities de ON e2.id = de.dup_id
WHERE de.keep_id = e.id),
0
)
WHERE e.id IN (SELECT keep_id FROM duplicate_entities);
-- Delete duplicate entities
DELETE FROM entities
WHERE id IN (SELECT dup_id FROM duplicate_entities);
-- Cleanup temp tables
DROP TABLE new_cooccurrences;
DROP TABLE duplicate_entities;
DROP TABLE canonical_entities;
""")
# Add unique constraint (case-insensitive)
op.create_index(
'idx_entities_agent_canonical_unique',
'entities',
[sa.text('agent_id'), sa.text('LOWER(canonical_name)')],
unique=True
)
def downgrade() -> None:
op.drop_index('idx_entities_agent_canonical_unique', table_name='entities')

View file

@ -1,30 +0,0 @@
"""remove_entity_type_column
Revision ID: 8c55f5602451
Revises: 2a76a5bc2f09
Create Date: 2025-11-07 17:08:07.329740
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '8c55f5602451'
down_revision: Union[str, Sequence[str], None] = '2a76a5bc2f09'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Remove entity_type column from entities table
op.execute("ALTER TABLE entities DROP COLUMN IF EXISTS entity_type")
def downgrade() -> None:
"""Downgrade schema."""
# Re-add entity_type column (default to 'OTHER' for existing rows)
op.execute("ALTER TABLE entities ADD COLUMN entity_type TEXT DEFAULT 'OTHER'")

View file

@ -1,70 +0,0 @@
"""add_temporal_ranges_to_memory_units
Revision ID: 9d42e6f91234
Revises: 8c55f5602451
Create Date: 2025-11-17 00:00:00.000000
This migration adds temporal range support to memory_units table:
- occurred_start: When the fact/event started
- occurred_end: When the fact/event ended
- mentioned_at: When the fact was mentioned/learned
For existing rows, these are initialized from event_date (point events).
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import TIMESTAMP
# revision identifiers, used by Alembic.
revision: str = '9d42e6f91234'
down_revision: Union[str, Sequence[str], None] = '8c55f5602451'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema: add temporal range columns to memory_units."""
# Add new temporal range columns (nullable initially)
op.add_column(
'memory_units',
sa.Column('occurred_start', TIMESTAMP(timezone=True), nullable=True)
)
op.add_column(
'memory_units',
sa.Column('occurred_end', TIMESTAMP(timezone=True), nullable=True)
)
op.add_column(
'memory_units',
sa.Column('mentioned_at', TIMESTAMP(timezone=True), nullable=True)
)
# Populate new columns from existing event_date for backward compatibility
# For existing facts, treat them as point events (start = end = event_date)
# and assume they were mentioned at the same time
op.execute("""
UPDATE memory_units
SET
occurred_start = event_date,
occurred_end = event_date,
mentioned_at = event_date
WHERE occurred_start IS NULL
""")
# Optional: Make columns non-nullable after populating
# Uncomment if you want to enforce NOT NULL constraint
# op.alter_column('memory_units', 'occurred_start', nullable=False)
# op.alter_column('memory_units', 'occurred_end', nullable=False)
# op.alter_column('memory_units', 'mentioned_at', nullable=False)
def downgrade() -> None:
"""Downgrade schema: remove temporal range columns from memory_units."""
# Remove the temporal range columns
op.drop_column('memory_units', 'mentioned_at')
op.drop_column('memory_units', 'occurred_end')
op.drop_column('memory_units', 'occurred_start')

View file

@ -1,196 +0,0 @@
"""Initial schema
Revision ID: af0413383b3e
Revises:
Create Date: 2025-11-03 14:31:53.245542
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
import pgvector.sqlalchemy
# revision identifiers, used by Alembic.
revision: str = 'af0413383b3e'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Create pgvector extension
op.execute('CREATE EXTENSION IF NOT EXISTS vector')
op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('documents',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('agent_id', sa.Text(), nullable=False),
sa.Column('original_text', sa.Text(), nullable=True),
sa.Column('content_hash', sa.Text(), nullable=True),
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()),
server_default=sa.text("'{}'::jsonb"), nullable=False),
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'),
nullable=False),
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'),
nullable=False),
sa.PrimaryKeyConstraint('id', 'agent_id')
)
op.create_index('idx_documents_agent_id', 'documents', ['agent_id'], unique=False)
op.create_index('idx_documents_content_hash', 'documents', ['content_hash'], unique=False)
op.create_table('entities',
sa.Column('id', sa.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('canonical_name', sa.Text(), nullable=False),
sa.Column('entity_type', sa.Text(), nullable=False),
sa.Column('agent_id', sa.Text(), nullable=False),
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()),
server_default=sa.text("'{}'::jsonb"), nullable=False),
sa.Column('first_seen', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'),
nullable=False),
sa.Column('last_seen', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'),
nullable=False),
sa.Column('mention_count', sa.Integer(), server_default='1', nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_entities_agent_id', 'entities', ['agent_id'], unique=False)
op.create_index('idx_entities_agent_name_type', 'entities', ['agent_id', 'canonical_name', 'entity_type'],
unique=False)
op.create_index('idx_entities_canonical_name', 'entities', ['canonical_name'], unique=False)
op.create_index('idx_entities_type', 'entities', ['entity_type'], unique=False)
op.create_table('entity_cooccurrences',
sa.Column('entity_id_1', sa.UUID(), nullable=False),
sa.Column('entity_id_2', sa.UUID(), nullable=False),
sa.Column('cooccurrence_count', sa.Integer(), server_default='1', nullable=False),
sa.Column('last_cooccurred', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'),
nullable=False),
sa.CheckConstraint('entity_id_1 < entity_id_2', name='entity_cooccurrence_order_check'),
sa.ForeignKeyConstraint(['entity_id_1'], ['entities.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['entity_id_2'], ['entities.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('entity_id_1', 'entity_id_2')
)
op.create_index('idx_entity_cooccurrences_count', 'entity_cooccurrences', ['cooccurrence_count'], unique=False,
postgresql_ops={'cooccurrence_count': 'DESC'})
op.create_index('idx_entity_cooccurrences_entity1', 'entity_cooccurrences', ['entity_id_1'], unique=False)
op.create_index('idx_entity_cooccurrences_entity2', 'entity_cooccurrences', ['entity_id_2'], unique=False)
op.create_table('memory_units',
sa.Column('id', sa.UUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('agent_id', sa.Text(), nullable=False),
sa.Column('document_id', sa.Text(), nullable=True),
sa.Column('text', sa.Text(), nullable=False),
sa.Column('embedding', pgvector.sqlalchemy.vector.VECTOR(dim=384), nullable=True),
sa.Column('context', sa.Text(), nullable=True),
sa.Column('event_date', postgresql.TIMESTAMP(timezone=True), nullable=False),
sa.Column('fact_type', sa.Text(), server_default='world', nullable=False),
sa.Column('confidence_score', sa.Float(), nullable=True),
sa.Column('access_count', sa.Integer(), server_default='0', nullable=False),
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'),
nullable=False),
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'),
nullable=False),
sa.CheckConstraint(
"(fact_type = 'opinion' AND confidence_score IS NOT NULL) OR (fact_type != 'opinion' AND confidence_score IS NULL)",
name='confidence_score_fact_type_check'),
sa.CheckConstraint("fact_type IN ('world', 'agent', 'opinion')"),
sa.CheckConstraint(
'confidence_score IS NULL OR (confidence_score >= 0.0 AND confidence_score <= 1.0)'),
sa.ForeignKeyConstraint(['document_id', 'agent_id'], ['documents.id', 'documents.agent_id'],
name='memory_units_document_fkey', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_memory_units_access_count', 'memory_units', ['access_count'], unique=False,
postgresql_ops={'access_count': 'DESC'})
op.create_index('idx_memory_units_agent_date', 'memory_units', ['agent_id', 'event_date'], unique=False,
postgresql_ops={'event_date': 'DESC'})
op.create_index('idx_memory_units_agent_fact_type', 'memory_units', ['agent_id', 'fact_type'], unique=False)
op.create_index('idx_memory_units_agent_id', 'memory_units', ['agent_id'], unique=False)
op.create_index('idx_memory_units_agent_type_date', 'memory_units', ['agent_id', 'fact_type', 'event_date'],
unique=False, postgresql_ops={'event_date': 'DESC'})
op.create_index('idx_memory_units_document_id', 'memory_units', ['document_id'], unique=False)
op.create_index('idx_memory_units_embedding', 'memory_units', ['embedding'], unique=False, postgresql_using='hnsw',
postgresql_ops={'embedding': 'vector_cosine_ops'})
op.create_index('idx_memory_units_event_date', 'memory_units', ['event_date'], unique=False,
postgresql_ops={'event_date': 'DESC'})
op.create_index('idx_memory_units_fact_type', 'memory_units', ['fact_type'], unique=False)
op.create_index('idx_memory_units_opinion_confidence', 'memory_units', ['agent_id', 'confidence_score'],
unique=False, postgresql_where=sa.text("fact_type = 'opinion'"),
postgresql_ops={'confidence_score': 'DESC'})
op.create_index('idx_memory_units_opinion_date', 'memory_units', ['agent_id', 'event_date'], unique=False,
postgresql_where=sa.text("fact_type = 'opinion'"), postgresql_ops={'event_date': 'DESC'})
op.create_table('memory_links',
sa.Column('from_unit_id', sa.UUID(), nullable=False),
sa.Column('to_unit_id', sa.UUID(), nullable=False),
sa.Column('link_type', sa.Text(), nullable=False),
sa.Column('entity_id', sa.UUID(), nullable=False),
sa.Column('weight', sa.Float(), server_default='1.0', nullable=False),
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'),
nullable=False),
sa.ForeignKeyConstraint(['entity_id'], ['entities.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['from_unit_id'], ['memory_units.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['to_unit_id'], ['memory_units.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('from_unit_id', 'to_unit_id', 'link_type', 'entity_id')
)
op.create_index('idx_memory_links_entity', 'memory_links', ['entity_id'], unique=False,
postgresql_where=sa.text('entity_id IS NOT NULL'))
op.create_index('idx_memory_links_from', 'memory_links', ['from_unit_id'], unique=False)
op.create_index('idx_memory_links_from_weight', 'memory_links', ['from_unit_id', 'weight'], unique=False,
postgresql_where=sa.text('weight >= 0.1'), postgresql_ops={'weight': 'DESC'})
op.create_index('idx_memory_links_to', 'memory_links', ['to_unit_id'], unique=False)
op.create_index('idx_memory_links_type', 'memory_links', ['link_type'], unique=False)
op.create_table('unit_entities',
sa.Column('unit_id', sa.UUID(), nullable=False),
sa.Column('entity_id', sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(['entity_id'], ['entities.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['unit_id'], ['memory_units.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('unit_id', 'entity_id')
)
op.create_index('idx_unit_entities_entity', 'unit_entities', ['entity_id'], unique=False)
op.create_index('idx_unit_entities_unit', 'unit_entities', ['unit_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('idx_unit_entities_unit', table_name='unit_entities')
op.drop_index('idx_unit_entities_entity', table_name='unit_entities')
op.drop_table('unit_entities')
op.drop_index('idx_memory_links_type', table_name='memory_links')
op.drop_index('idx_memory_links_to', table_name='memory_links')
op.drop_index('idx_memory_links_from_weight', table_name='memory_links', postgresql_where=sa.text('weight >= 0.1'),
postgresql_ops={'weight': 'DESC'})
op.drop_index('idx_memory_links_from', table_name='memory_links')
op.drop_index('idx_memory_links_entity', table_name='memory_links',
postgresql_where=sa.text('entity_id IS NOT NULL'))
op.drop_table('memory_links')
op.drop_index('idx_memory_units_opinion_date', table_name='memory_units',
postgresql_where=sa.text("fact_type = 'opinion'"), postgresql_ops={'event_date': 'DESC'})
op.drop_index('idx_memory_units_opinion_confidence', table_name='memory_units',
postgresql_where=sa.text("fact_type = 'opinion'"), postgresql_ops={'confidence_score': 'DESC'})
op.drop_index('idx_memory_units_fact_type', table_name='memory_units')
op.drop_index('idx_memory_units_event_date', table_name='memory_units', postgresql_ops={'event_date': 'DESC'})
op.drop_index('idx_memory_units_embedding', table_name='memory_units', postgresql_using='hnsw',
postgresql_ops={'embedding': 'vector_cosine_ops'})
op.drop_index('idx_memory_units_document_id', table_name='memory_units')
op.drop_index('idx_memory_units_agent_type_date', table_name='memory_units', postgresql_ops={'event_date': 'DESC'})
op.drop_index('idx_memory_units_agent_id', table_name='memory_units')
op.drop_index('idx_memory_units_agent_fact_type', table_name='memory_units')
op.drop_index('idx_memory_units_agent_date', table_name='memory_units', postgresql_ops={'event_date': 'DESC'})
op.drop_index('idx_memory_units_access_count', table_name='memory_units', postgresql_ops={'access_count': 'DESC'})
op.drop_table('memory_units')
op.drop_index('idx_entity_cooccurrences_entity2', table_name='entity_cooccurrences')
op.drop_index('idx_entity_cooccurrences_entity1', table_name='entity_cooccurrences')
op.drop_index('idx_entity_cooccurrences_count', table_name='entity_cooccurrences',
postgresql_ops={'cooccurrence_count': 'DESC'})
op.drop_table('entity_cooccurrences')
op.drop_index('idx_entities_type', table_name='entities')
op.drop_index('idx_entities_canonical_name', table_name='entities')
op.drop_index('idx_entities_agent_name_type', table_name='entities')
op.drop_index('idx_entities_agent_id', table_name='entities')
op.drop_table('entities')
op.drop_index('idx_documents_content_hash', table_name='documents')
op.drop_index('idx_documents_agent_id', table_name='documents')
op.drop_table('documents')
# ### end Alembic commands ###

View file

@ -80,26 +80,26 @@ def create_app(
# Re-export commonly used items for backwards compatibility
from .http import (
SearchRequest,
SearchResult,
SearchResponse,
RecallRequest,
RecallResult,
RecallResponse,
MemoryItem,
BatchPutRequest,
ThinkRequest,
ThinkResponse,
CreateAgentRequest,
RetainRequest,
ReflectRequest,
ReflectResponse,
CreateBankRequest,
PersonalityTraits,
)
__all__ = [
"create_app",
"SearchRequest",
"SearchResult",
"SearchResponse",
"RecallRequest",
"RecallResult",
"RecallResponse",
"MemoryItem",
"BatchPutRequest",
"ThinkRequest",
"ThinkResponse",
"CreateAgentRequest",
"RetainRequest",
"ReflectRequest",
"ReflectResponse",
"CreateBankRequest",
"PersonalityTraits",
]

File diff suppressed because it is too large Load diff

View file

@ -24,15 +24,15 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
mcp = FastMCP("hindsight-mcp-server")
@mcp.tool()
async def hindsight_put(agent_id: str, content: str, context: str, explanation: str = "") -> str:
async def hindsight_put(bank_id: str, content: str, context: str, explanation: str = "") -> str:
"""
**CRITICAL: Store important user information to long-term memory.**
** PER-USER TOOL - REQUIRES USER IDENTIFICATION:**
- This tool is STRICTLY per-user. Each user MUST have a unique `agent_id`.
- ONLY use this tool if you have a valid user identifier (user ID, email, session ID, etc.) to map to `agent_id`.
- This tool is STRICTLY per-user. Each user MUST have a unique `bank_id`.
- ONLY use this tool if you have a valid user identifier (user ID, email, session ID, etc.) to map to `bank_id`.
- DO NOT use this tool if you cannot identify the specific user.
- DO NOT share memories between different users - each user's memories are isolated by their `agent_id`.
- DO NOT share memories between different users - each user's memories are isolated by their `bank_id`.
- If you don't have a user identifier, DO NOT use this tool at all.
Use this tool PROACTIVELY whenever the user shares:
@ -51,7 +51,7 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
"career_goals", "project_details", etc. This helps organize and retrieve related memories later.
Args:
agent_id: **REQUIRED** - The unique, persistent identifier for this specific user (e.g., user_id, email, session_id).
bank_id: **REQUIRED** - The unique, persistent identifier for this specific user (e.g., user_id, email, session_id).
This MUST be consistent across all interactions with the same user.
Example: "user_12345", "alice@example.com", "session_abc123"
content: The fact/memory to store (be specific and include relevant details)
@ -65,7 +65,7 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
# Store memory using put_batch_async
await memory.put_batch_async(
agent_id=agent_id,
bank_id=bank_id,
contents=[{"content": content, "context": context}]
)
return f"Fact stored successfully"
@ -74,15 +74,15 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
return f"Error: {str(e)}"
@mcp.tool()
async def hindsight_search(agent_id: str, query: str, max_tokens: int = 4096, explanation: str = "") -> str:
async def hindsight_search(bank_id: str, query: str, max_tokens: int = 4096, explanation: str = "") -> str:
"""
**CRITICAL: Search user's memory to provide personalized, context-aware responses.**
** PER-USER TOOL - REQUIRES USER IDENTIFICATION:**
- This tool is STRICTLY per-user. Each user MUST have a unique `agent_id`.
- ONLY use this tool if you have a valid user identifier (user ID, email, session ID, etc.) to map to `agent_id`.
- This tool is STRICTLY per-user. Each user MUST have a unique `bank_id`.
- ONLY use this tool if you have a valid user identifier (user ID, email, session ID, etc.) to map to `bank_id`.
- DO NOT use this tool if you cannot identify the specific user.
- DO NOT search across multiple users - each user's memories are isolated by their `agent_id`.
- DO NOT search across multiple users - each user's memories are isolated by their `bank_id`.
- If you don't have a user identifier, DO NOT use this tool at all.
Use this tool PROACTIVELY at the start of conversations or when making recommendations to:
@ -103,7 +103,7 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
"user's work experience", "user's dietary restrictions", "what does the user know about X?"
Args:
agent_id: **REQUIRED** - The unique, persistent identifier for this specific user (e.g., user_id, email, session_id).
bank_id: **REQUIRED** - The unique, persistent identifier for this specific user (e.g., user_id, email, session_id).
This MUST be consistent across all interactions with the same user.
Example: "user_12345", "alice@example.com", "session_abc123"
query: Natural language search query to find relevant memories
@ -118,13 +118,14 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
if explanation:
logger.debug(f"Explanation: {explanation}")
# Search using search_async
search_result = await memory.search_async(
agent_id=agent_id,
# Search using recall_async
from hindsight_api.engine.memory_engine import Budget
search_result = await memory.recall_async(
bank_id=bank_id,
query=query,
fact_type=["world", "agent", "opinion"], # Search all fact types
fact_type=["world", "bank", "opinion"], # Search all fact types
max_tokens=max_tokens,
thinking_budget=100
budget=Budget.LOW
)
# Convert results to dict format

View file

@ -3,7 +3,7 @@ Memory Engine - Core implementation of the memory system.
This package contains all the implementation details of the memory engine:
- MemoryEngine: Main class for memory operations
- Utility modules: embedding_utils, link_utils, think_utils, agent_utils
- Utility modules: embedding_utils, link_utils, think_utils, bank_utils
- Supporting modules: embeddings, cross_encoder, entity_resolver, etc.
"""
@ -23,7 +23,7 @@ from .search_trace import (
)
from .search_tracer import SearchTracer
from .llm_wrapper import LLMConfig
from .response_models import SearchResult, ThinkResult, MemoryFact
from .response_models import RecallResult, ReflectResult, MemoryFact
__all__ = [
"MemoryEngine",
@ -41,7 +41,7 @@ __all__ = [
"SearchSummary",
"SearchPhaseMetrics",
"LLMConfig",
"SearchResult",
"ThinkResult",
"RecallResult",
"ReflectResult",
"MemoryFact",
]

View file

@ -1,5 +1,5 @@
"""
Agent profile utilities for personality and background management.
bank profile utilities for personality and background management.
"""
import json
@ -37,27 +37,26 @@ class BackgroundMergeResponse(BaseModel):
personality: PersonalityTraits = Field(description="Inferred Big Five personality traits")
async def get_agent_profile(pool, agent_id: str) -> Dict:
async def get_bank_profile(pool, bank_id: str) -> Dict:
"""
Get agent profile (name, personality + background).
Auto-creates agent with default values if not exists.
Get bank profile (name, personality + background).
Auto-creates bank with default values if not exists.
Args:
pool: Database connection pool
agent_id: Agent identifier
bank_id: bank IDentifier
Returns:
Dict with 'name' (str), 'personality' (dict) and 'background' (str) keys
"""
async with acquire_with_retry(pool) as conn:
# Try to get existing agent
# Try to get existing bank
row = await conn.fetchrow(
"""
SELECT name, personality, background
FROM agents
WHERE agent_id = $1
FROM banks WHERE bank_id = $1
""",
agent_id
bank_id
)
if row:
@ -72,59 +71,59 @@ async def get_agent_profile(pool, agent_id: str) -> Dict:
"background": row["background"]
}
# Agent doesn't exist, create with defaults
# Bank doesn't exist, create with defaults
await conn.execute(
"""
INSERT INTO agents (agent_id, name, personality, background)
INSERT INTO banks (bank_id, name, personality, background)
VALUES ($1, $2, $3::jsonb, $4)
ON CONFLICT (agent_id) DO NOTHING
ON CONFLICT (bank_id) DO NOTHING
""",
agent_id,
agent_id, # Default name is the agent_id
bank_id,
bank_id, # Default name is the bank_id
json.dumps(DEFAULT_PERSONALITY),
""
)
return {
"name": agent_id,
"name": bank_id,
"personality": DEFAULT_PERSONALITY.copy(),
"background": ""
}
async def update_agent_personality(
async def update_bank_personality(
pool,
agent_id: str,
bank_id: str,
personality: Dict[str, float]
) -> None:
"""
Update agent personality traits.
Update bank personality traits.
Args:
pool: Database connection pool
agent_id: Agent identifier
bank_id: bank IDentifier
personality: Dict with Big Five traits + bias_strength (all 0-1)
"""
# Ensure agent exists first
await get_agent_profile(pool, agent_id)
# Ensure bank exists first
await get_bank_profile(pool, bank_id)
async with acquire_with_retry(pool) as conn:
await conn.execute(
"""
UPDATE agents
UPDATE banks
SET personality = $2::jsonb,
updated_at = NOW()
WHERE agent_id = $1
WHERE bank_id = $1
""",
agent_id,
bank_id,
json.dumps(personality)
)
async def merge_agent_background(
async def merge_bank_background(
pool,
llm_config,
agent_id: str,
bank_id: str,
new_info: str,
update_personality: bool = True
) -> dict:
@ -136,7 +135,7 @@ async def merge_agent_background(
Args:
pool: Database connection pool
llm_config: LLM configuration for background merging
agent_id: Agent identifier
bank_id: bank IDentifier
new_info: New background information to add/merge
update_personality: If True, infer Big Five traits from background (default: True)
@ -144,7 +143,7 @@ async def merge_agent_background(
Dict with 'background' (str) and optionally 'personality' (dict) keys
"""
# Get current profile
profile = await get_agent_profile(pool, agent_id)
profile = await get_bank_profile(pool, bank_id)
current_background = profile["background"]
# Use LLM to merge backgrounds and optionally infer personality
@ -164,13 +163,13 @@ async def merge_agent_background(
# Update both background and personality
await conn.execute(
"""
UPDATE agents
UPDATE banks
SET background = $2,
personality = $3::jsonb,
updated_at = NOW()
WHERE agent_id = $1
WHERE bank_id = $1
""",
agent_id,
bank_id,
merged_background,
json.dumps(inferred_personality)
)
@ -178,12 +177,12 @@ async def merge_agent_background(
# Update only background
await conn.execute(
"""
UPDATE agents
UPDATE banks
SET background = $2,
updated_at = NOW()
WHERE agent_id = $1
WHERE bank_id = $1
""",
agent_id,
bank_id,
merged_background
)
@ -214,7 +213,7 @@ async def _llm_merge_background(
Dict with 'background' (str) and optionally 'personality' (dict) keys
"""
if infer_personality:
prompt = f"""You are helping maintain an agent's background/profile and infer their personality. You MUST respond with ONLY valid JSON.
prompt = f"""You are helping maintain a memory bank's background/profile and infer their personality. You MUST respond with ONLY valid JSON.
Current background: {current if current else "(empty)"}
@ -257,7 +256,7 @@ Trait inference examples:
- "rational and diligent" conscientiousness: 0.7+, openness: 0.6+
- "passionate and dramatic" extraversion: 0.7+, neuroticism: 0.6+, openness: 0.7+"""
else:
prompt = f"""You are helping maintain an agent's background/profile.
prompt = f"""You are helping maintain a memory bank's background/profile.
Current background: {current if current else "(empty)"}
@ -283,7 +282,7 @@ Merged background:"""
parsed = await llm_config.call(
messages=messages,
response_format=BackgroundMergeResponse,
scope="agent_background",
scope="bank_background",
temperature=0.3,
max_tokens=8192
)
@ -301,7 +300,7 @@ Merged background:"""
# Manual parsing fallback or non-personality merge
content = await llm_config.call(
messages=messages,
scope="agent_background",
scope="bank_background",
temperature=0.3,
max_tokens=8192
)
@ -388,21 +387,21 @@ Merged background:"""
return result
async def list_agents(pool) -> list:
async def list_banks(pool) -> list:
"""
List all agents in the system.
List all banks in the system.
Args:
pool: Database connection pool
Returns:
List of dicts with agent_id, name, personality, background, created_at, updated_at
List of dicts with bank_id, name, personality, background, created_at, updated_at
"""
async with acquire_with_retry(pool) as conn:
rows = await conn.fetch(
"""
SELECT agent_id, name, personality, background, created_at, updated_at
FROM agents
SELECT bank_id, name, personality, background, created_at, updated_at
FROM banks
ORDER BY updated_at DESC
"""
)
@ -415,7 +414,7 @@ async def list_agents(pool) -> list:
personality_data = json.loads(personality_data)
result.append({
"agent_id": row["agent_id"],
"bank_id": row["bank_id"],
"name": row["name"],
"personality": personality_data,
"background": row["background"],

View file

@ -31,7 +31,7 @@ class EntityResolver:
async def resolve_entities_batch(
self,
agent_id: str,
bank_id: str,
entities_data: List[Dict],
context: str,
unit_event_date,
@ -44,7 +44,7 @@ class EntityResolver:
all entities with minimal DB queries.
Args:
agent_id: Agent ID
bank_id: bank ID
entities_data: List of dicts with 'text', 'type', 'nearby_entities'
context: Context where entities appear
unit_event_date: When this unit was created
@ -58,34 +58,34 @@ class EntityResolver:
if conn is None:
async with acquire_with_retry(self.pool) as conn:
return await self._resolve_entities_batch_impl(conn, agent_id, entities_data, context, unit_event_date)
return await self._resolve_entities_batch_impl(conn, bank_id, entities_data, context, unit_event_date)
else:
return await self._resolve_entities_batch_impl(conn, agent_id, entities_data, context, unit_event_date)
return await self._resolve_entities_batch_impl(conn, bank_id, entities_data, context, unit_event_date)
async def _resolve_entities_batch_impl(self, conn, agent_id: str, entities_data: List[Dict], context: str, unit_event_date) -> List[str]:
# Query ALL candidates for this agent
async def _resolve_entities_batch_impl(self, conn, bank_id: str, entities_data: List[Dict], context: str, unit_event_date) -> List[str]:
# Query ALL candidates for this bank
all_entities = await conn.fetch(
"""
SELECT canonical_name, id, metadata, last_seen, mention_count
FROM entities
WHERE agent_id = $1
WHERE bank_id = $1
""",
agent_id
bank_id
)
# Build entity ID to name mapping for co-occurrence lookups
entity_id_to_name = {row['id']: row['canonical_name'].lower() for row in all_entities}
# Query ALL co-occurrences for this agent's entities in one query
# Query ALL co-occurrences for this bank's entities in one query
# This builds a map of entity_id -> set of co-occurring entity names
all_cooccurrences = await conn.fetch(
"""
SELECT ec.entity_id_1, ec.entity_id_2, ec.cooccurrence_count
FROM entity_cooccurrences ec
WHERE ec.entity_id_1 IN (SELECT id FROM entities WHERE agent_id = $1)
OR ec.entity_id_2 IN (SELECT id FROM entities WHERE agent_id = $1)
WHERE ec.entity_id_1 IN (SELECT id FROM entities WHERE bank_id = $1)
OR ec.entity_id_2 IN (SELECT id FROM entities WHERE bank_id = $1)
""",
agent_id
bank_id
)
# Build co-occurrence map: entity_id -> set of co-occurring entity names (lowercase)
@ -205,18 +205,18 @@ class EntityResolver:
if entities_to_create:
for idx, entity_data in entities_to_create:
# Use INSERT ... ON CONFLICT to atomically get-or-create
# The unique index is on (agent_id, LOWER(canonical_name))
# The unique index is on (bank_id, LOWER(canonical_name))
row = await conn.fetchrow(
"""
INSERT INTO entities (agent_id, canonical_name, first_seen, last_seen, mention_count)
INSERT INTO entities (bank_id, canonical_name, first_seen, last_seen, mention_count)
VALUES ($1, $2, $3, $4, 1)
ON CONFLICT (agent_id, LOWER(canonical_name))
ON CONFLICT (bank_id, LOWER(canonical_name))
DO UPDATE SET
mention_count = entities.mention_count + 1,
last_seen = EXCLUDED.last_seen
RETURNING id
""",
agent_id,
bank_id,
entity_data['text'],
unit_event_date,
unit_event_date
@ -227,7 +227,7 @@ class EntityResolver:
async def resolve_entity(
self,
agent_id: str,
bank_id: str,
entity_text: str,
context: str,
nearby_entities: List[Dict],
@ -237,7 +237,7 @@ class EntityResolver:
Resolve an entity to a canonical entity ID.
Args:
agent_id: Agent ID (entities are scoped to agents)
bank_id: bank ID (entities are scoped to agents)
entity_text: Entity text ("Alice", "Google", etc.)
context: Context where entity appears
nearby_entities: Other entities in the same unit
@ -252,7 +252,7 @@ class EntityResolver:
"""
SELECT id, canonical_name, metadata, last_seen
FROM entities
WHERE agent_id = $1
WHERE bank_id = $1
AND (
canonical_name ILIKE $2
OR canonical_name ILIKE $3
@ -260,13 +260,13 @@ class EntityResolver:
)
ORDER BY mention_count DESC
""",
agent_id, entity_text, f"%{entity_text}%"
bank_id, entity_text, f"%{entity_text}%"
)
if not candidates:
# New entity - create it
return await self._create_entity(
conn, agent_id, entity_text, unit_event_date
conn, bank_id, entity_text, unit_event_date
)
# Score candidates based on:
@ -351,13 +351,13 @@ class EntityResolver:
else:
# Not confident - create new entity
return await self._create_entity(
conn, agent_id, entity_text, unit_event_date
conn, bank_id, entity_text, unit_event_date
)
async def _create_entity(
self,
conn,
agent_id: str,
bank_id: str,
entity_text: str,
event_date,
) -> str:
@ -369,7 +369,7 @@ class EntityResolver:
Args:
conn: Database connection
agent_id: Agent ID
bank_id: bank ID
entity_text: Entity text
event_date: When first seen
@ -378,15 +378,15 @@ class EntityResolver:
"""
entity_id = await conn.fetchval(
"""
INSERT INTO entities (agent_id, canonical_name, first_seen, last_seen, mention_count)
INSERT INTO entities (bank_id, canonical_name, first_seen, last_seen, mention_count)
VALUES ($1, $2, $3, $4, 1)
ON CONFLICT (agent_id, LOWER(canonical_name))
ON CONFLICT (bank_id, LOWER(canonical_name))
DO UPDATE SET
mention_count = entities.mention_count + 1,
last_seen = EXCLUDED.last_seen
RETURNING id
""",
agent_id, entity_text, event_date, event_date
bank_id, entity_text, event_date, event_date
)
return entity_id
@ -547,14 +547,14 @@ class EntityResolver:
async def get_entity_by_text(
self,
agent_id: str,
bank_id: str,
entity_text: str,
) -> Optional[str]:
"""
Find an entity by text (for query resolution).
Args:
agent_id: Agent ID
bank_id: bank ID
entity_text: Entity text to search for
Returns:
@ -564,12 +564,12 @@ class EntityResolver:
row = await conn.fetchrow(
"""
SELECT id FROM entities
WHERE agent_id = $1
WHERE bank_id = $1
AND canonical_name ILIKE $2
ORDER BY mention_count DESC
LIMIT 1
""",
agent_id, entity_text
bank_id, entity_text
)
return row['id'] if row else None

View file

@ -91,8 +91,8 @@ class ExtractedFact(BaseModel):
)
# Classification
fact_type: Literal["world", "agent", "opinion"] = Field(
description="'world' = facts about others (third person), 'agent' = facts about YOU the memory owner (FIRST PERSON: 'I did...'), 'opinion' = your beliefs (first person)"
fact_type: Literal["world", "bank", "opinion"] = Field(
description="'world' = facts about others (third person), 'bank' = facts about YOU the memory owner (FIRST PERSON: 'I did...'), 'opinion' = your beliefs (first person)"
)
# Entities and relations
@ -199,9 +199,9 @@ async def _extract_facts_from_chunk(
# Determine which fact types to extract based on the flag
if extract_opinions:
fact_types_instruction = "Extract ONLY 'opinion' type facts (the agent's formed opinions, beliefs, and perspectives). DO NOT extract 'world' or 'agent' facts."
fact_types_instruction = "Extract ONLY 'opinion' type facts (the bank's formed opinions, beliefs, and perspectives). DO NOT extract 'world' or 'bank' facts."
else:
fact_types_instruction = "Extract ONLY 'world' and 'agent' type facts. DO NOT extract 'opinion' type facts - opinions should never be created during normal memory storage."
fact_types_instruction = "Extract ONLY 'world' and 'bank' type facts. DO NOT extract 'opinion' type facts - opinions should never be created during normal memory storage."
prompt = f"""You are extracting comprehensive, narrative facts from conversations/document for an AI memory system.

View file

@ -24,7 +24,7 @@ def _log(log_buffer, message, level='info'):
async def extract_entities_batch_optimized(
entity_resolver,
conn,
agent_id: str,
bank_id: str,
unit_ids: List[str],
sentences: List[str],
context: str,
@ -41,7 +41,7 @@ async def extract_entities_batch_optimized(
Args:
entity_resolver: EntityResolver instance for entity resolution
conn: Database connection
agent_id: Agent identifier
agent_id: bank IDentifier
unit_ids: List of unit IDs
sentences: List of fact sentences
context: Context string
@ -114,7 +114,7 @@ async def extract_entities_batch_optimized(
entities_data = [entity_data for _, entity_data in entities_group]
batch_resolved = await entity_resolver.resolve_entities_batch(
agent_id=agent_id,
bank_id=bank_id,
entities_data=entities_data,
context=context,
unit_event_date=fact_date,
@ -209,7 +209,7 @@ async def extract_entities_batch_optimized(
async def create_temporal_links_batch_per_fact(
conn,
agent_id: str,
bank_id: str,
unit_ids: List[str],
time_window_hours: int = 24,
log_buffer: List[str] = None,
@ -222,7 +222,7 @@ async def create_temporal_links_batch_per_fact(
Args:
conn: Database connection
agent_id: Agent identifier
agent_id: bank IDentifier
unit_ids: List of unit IDs
time_window_hours: Time window in hours for temporal links
log_buffer: Optional buffer for logging
@ -257,12 +257,12 @@ async def create_temporal_links_batch_per_fact(
"""
SELECT id, event_date
FROM memory_units
WHERE agent_id = $1
WHERE bank_id = $1
AND event_date BETWEEN $2 AND $3
AND id::text != ALL($4)
ORDER BY event_date DESC
""",
agent_id,
bank_id,
min_date,
max_date,
unit_ids
@ -312,7 +312,7 @@ async def create_temporal_links_batch_per_fact(
async def create_semantic_links_batch(
conn,
agent_id: str,
bank_id: str,
unit_ids: List[str],
embeddings: List[List[float]],
top_k: int = 5,
@ -326,7 +326,7 @@ async def create_semantic_links_batch(
Args:
conn: Database connection
agent_id: Agent identifier
agent_id: bank IDentifier
unit_ids: List of unit IDs
embeddings: List of embedding vectors
top_k: Number of top similar units to link
@ -346,11 +346,11 @@ async def create_semantic_links_batch(
"""
SELECT id, embedding
FROM memory_units
WHERE agent_id = $1
WHERE bank_id = $1
AND embedding IS NOT NULL
AND id::text != ALL($2)
""",
agent_id,
bank_id,
unit_ids
)
_log(log_buffer, f" [8.1] Fetch {len(all_existing)} existing embeddings (1 query): {time_mod.time() - fetch_start:.3f}s")

File diff suppressed because it is too large Load diff

View file

@ -35,7 +35,7 @@ class MemoryFact(BaseModel):
id: str = Field(description="Unique identifier for the memory fact")
text: str = Field(description="The actual text content of the memory")
fact_type: str = Field(description="Type of fact: 'world', 'agent', 'opinion', or 'observation'")
fact_type: str = Field(description="Type of fact: 'world', 'bank', 'opinion', or 'observation'")
entities: Optional[List[str]] = Field(None, description="Entity names mentioned in this fact")
context: Optional[str] = Field(None, description="Additional context for the memory")
occurred_start: Optional[str] = Field(None, description="ISO format date when the event started occurring")
@ -48,9 +48,9 @@ class MemoryFact(BaseModel):
activation: Optional[float] = Field(None, description="Internal activation score")
class SearchResult(BaseModel):
class RecallResult(BaseModel):
"""
Result from a search operation.
Result from a recall operation.
Contains a list of matching memory facts and optional trace information
for debugging and transparency.
@ -83,12 +83,12 @@ class SearchResult(BaseModel):
)
class ThinkResult(BaseModel):
class ReflectResult(BaseModel):
"""
Result from a think operation.
Result from a reflect operation.
Contains the formulated answer, the facts it was based on (organized by type),
and any new opinions that were formed during the thinking process.
and any new opinions that were formed during the reflection process.
"""
model_config = ConfigDict(json_schema_extra={
"example": {
@ -119,7 +119,7 @@ class ThinkResult(BaseModel):
)
new_opinions: List[str] = Field(
default_factory=list,
description="List of newly formed opinions during thinking"
description="List of newly formed opinions during reflection"
)
@ -127,7 +127,7 @@ class Opinion(BaseModel):
"""
An opinion with confidence score.
Opinions represent the agent's formed perspectives on topics,
Opinions represent the bank's formed perspectives on topics,
with a confidence level indicating strength of belief.
"""
model_config = ConfigDict(json_schema_extra={

View file

@ -17,7 +17,7 @@ from ..db_utils import acquire_with_retry
async def retrieve_semantic(
conn,
query_emb_str: str,
agent_id: str,
bank_id: str,
fact_type: str,
limit: int
) -> List[Tuple[str, Dict[str, Any]]]:
@ -27,7 +27,7 @@ async def retrieve_semantic(
Args:
conn: Database connection
query_emb_str: Query embedding as string
agent_id: Agent ID
agent_id: bank ID
fact_type: Fact type to filter
limit: Maximum results to return
@ -39,14 +39,14 @@ async def retrieve_semantic(
SELECT id, text, context, event_date, occurred_start, occurred_end, mentioned_at, access_count, embedding, fact_type, document_id,
1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE agent_id = $2
WHERE bank_id = $2
AND embedding IS NOT NULL
AND fact_type = $3
AND (1 - (embedding <=> $1::vector)) >= 0.3
ORDER BY embedding <=> $1::vector
LIMIT $4
""",
query_emb_str, agent_id, fact_type, limit
query_emb_str, bank_id, fact_type, limit
)
return [(str(r["id"]), dict(r)) for r in results]
@ -54,7 +54,7 @@ async def retrieve_semantic(
async def retrieve_bm25(
conn,
query_text: str,
agent_id: str,
bank_id: str,
fact_type: str,
limit: int
) -> List[Tuple[str, Dict[str, Any]]]:
@ -64,7 +64,7 @@ async def retrieve_bm25(
Args:
conn: Database connection
query_text: Query text
agent_id: Agent ID
agent_id: bank ID
fact_type: Fact type to filter
limit: Maximum results to return
@ -93,13 +93,13 @@ async def retrieve_bm25(
SELECT id, text, context, event_date, occurred_start, occurred_end, mentioned_at, access_count, embedding, fact_type, document_id,
ts_rank_cd(search_vector, to_tsquery('english', $1)) AS bm25_score
FROM memory_units
WHERE agent_id = $2
WHERE bank_id = $2
AND fact_type = $3
AND search_vector @@ to_tsquery('english', $1)
ORDER BY bm25_score DESC
LIMIT $4
""",
query_tsquery, agent_id, fact_type, limit
query_tsquery, bank_id, fact_type, limit
)
return [(str(r["id"]), dict(r)) for r in results]
@ -107,7 +107,7 @@ async def retrieve_bm25(
async def retrieve_graph(
conn,
query_emb_str: str,
agent_id: str,
bank_id: str,
fact_type: str,
budget: int
) -> List[Tuple[str, Dict[str, Any]]]:
@ -117,7 +117,7 @@ async def retrieve_graph(
Args:
conn: Database connection
query_emb_str: Query embedding as string
agent_id: Agent ID
agent_id: bank ID
fact_type: Fact type to filter
budget: Node budget for graph traversal
@ -130,14 +130,14 @@ async def retrieve_graph(
SELECT id, text, context, event_date, occurred_start, occurred_end, mentioned_at, access_count, embedding, fact_type, document_id,
1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE agent_id = $2
WHERE bank_id = $2
AND embedding IS NOT NULL
AND fact_type = $3
AND (1 - (embedding <=> $1::vector)) >= 0.5
ORDER BY embedding <=> $1::vector
LIMIT 5
""",
query_emb_str, agent_id, fact_type
query_emb_str, bank_id, fact_type
)
if not entry_points:
@ -221,7 +221,7 @@ async def retrieve_graph(
async def retrieve_temporal(
conn,
query_emb_str: str,
agent_id: str,
bank_id: str,
fact_type: str,
start_date: datetime,
end_date: datetime,
@ -239,7 +239,7 @@ async def retrieve_temporal(
Args:
conn: Database connection
query_emb_str: Query embedding as string
agent_id: Agent ID
agent_id: bank ID
fact_type: Fact type to filter
start_date: Start of time range
end_date: End of time range
@ -262,7 +262,7 @@ async def retrieve_temporal(
SELECT id, text, context, event_date, occurred_start, occurred_end, mentioned_at, access_count, embedding, fact_type, document_id,
1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE agent_id = $2
WHERE bank_id = $2
AND fact_type = $3
AND embedding IS NOT NULL
AND (
@ -282,16 +282,16 @@ async def retrieve_temporal(
ORDER BY COALESCE(occurred_start, mentioned_at, occurred_end) DESC, (embedding <=> $1::vector) ASC
LIMIT 10
""",
query_emb_str, agent_id, fact_type, start_date, end_date, semantic_threshold
query_emb_str, bank_id, fact_type, start_date, end_date, semantic_threshold
)
if not entry_points:
# Check if there are ANY memories with temporal metadata for this agent
# Check if there are ANY memories with temporal metadata for this bank
total_with_dates = await conn.fetchval(
"""SELECT COUNT(*) FROM memory_units
WHERE agent_id = $1 AND fact_type = $2
WHERE bank_id = $1 AND fact_type = $2
AND (occurred_start IS NOT NULL OR occurred_end IS NOT NULL OR mentioned_at IS NOT NULL)""",
agent_id, fact_type
bank_id, fact_type
)
return []
@ -418,7 +418,7 @@ async def retrieve_parallel(
pool,
query_text: str,
query_embedding_str: str,
agent_id: str,
bank_id: str,
fact_type: str,
thinking_budget: int,
question_date: Optional[datetime] = None,
@ -431,7 +431,7 @@ async def retrieve_parallel(
pool: Database connection pool
query_text: Query text
query_embedding_str: Query embedding as string
agent_id: Agent ID
agent_id: bank ID
fact_type: Fact type to filter
thinking_budget: Budget for graph traversal and retrieval limits
question_date: Optional date when question was asked (for temporal filtering)
@ -461,20 +461,20 @@ async def retrieve_parallel(
async def run_semantic():
async with acquire_with_retry(pool) as conn:
return await retrieve_semantic(conn, query_embedding_str, agent_id, fact_type, limit=thinking_budget)
return await retrieve_semantic(conn, query_embedding_str, bank_id, fact_type, limit=thinking_budget)
async def run_bm25():
async with acquire_with_retry(pool) as conn:
return await retrieve_bm25(conn, query_text, agent_id, fact_type, limit=thinking_budget)
return await retrieve_bm25(conn, query_text, bank_id, fact_type, limit=thinking_budget)
async def run_graph():
async with acquire_with_retry(pool) as conn:
return await retrieve_graph(conn, query_embedding_str, agent_id, fact_type, budget=thinking_budget)
return await retrieve_graph(conn, query_embedding_str, bank_id, fact_type, budget=thinking_budget)
async def run_temporal(start_date, end_date):
async with acquire_with_retry(pool) as conn:
return await retrieve_temporal(
conn, query_embedding_str, agent_id, fact_type,
conn, query_embedding_str, bank_id, fact_type,
start_date, end_date, budget=thinking_budget, semantic_threshold=0.4
)

View file

@ -14,7 +14,7 @@ class QueryInfo(BaseModel):
query_text: str = Field(description="Original query text")
query_embedding: List[float] = Field(description="Generated query embedding vector")
timestamp: datetime = Field(description="When the query was executed")
thinking_budget: int = Field(description="Maximum nodes to explore")
budget: int = Field(description="Maximum nodes to explore")
max_tokens: int = Field(description="Maximum tokens to return in results")

View file

@ -30,7 +30,7 @@ class SearchTracer:
Tracer for collecting detailed search execution information.
Usage:
tracer = SearchTracer(query="Who is Alice?", thinking_budget=50, top_k=10)
tracer = SearchTracer(query="Who is Alice?", budget=50, max_tokens=4096)
tracer.start()
# During search...
@ -44,17 +44,17 @@ class SearchTracer:
json_output = trace.to_json()
"""
def __init__(self, query: str, thinking_budget: int, max_tokens: int):
def __init__(self, query: str, budget: int, max_tokens: int):
"""
Initialize tracer.
Args:
query: Search query text
thinking_budget: Maximum nodes to explore
budget: Maximum nodes to explore
max_tokens: Maximum tokens to return in results
"""
self.query_text = query
self.thinking_budget = thinking_budget
self.budget = budget
self.max_tokens = max_tokens
# Trace data
@ -400,7 +400,7 @@ class SearchTracer:
query_text=self.query_text,
query_embedding=self.query_embedding or [],
timestamp=datetime.now(timezone.utc),
thinking_budget=self.thinking_budget,
budget=self.budget,
max_tokens=self.max_tokens,
)
@ -410,7 +410,7 @@ class SearchTracer:
total_nodes_pruned=len(self.pruned),
entry_points_found=len(self.entry_points),
budget_used=len(self.visits),
budget_remaining=self.thinking_budget - len(self.visits),
budget_remaining=self.budget - len(self.visits),
total_duration_seconds=total_duration,
results_returned=len(final_results),
temporal_links_followed=self.temporal_links_followed,

View file

@ -9,13 +9,13 @@ from datetime import datetime, timezone
from typing import Dict, List, Any
from pydantic import BaseModel, Field
from .response_models import ThinkResult, MemoryFact
from .response_models import ReflectResult, MemoryFact
logger = logging.getLogger(__name__)
class Opinion(BaseModel):
"""An opinion formed by the agent."""
"""An opinion formed by the bank."""
opinion: str = Field(description="The opinion or perspective with reasoning included")
confidence: float = Field(description="Confidence score for this opinion (0.0 to 1.0, where 1.0 is very confident)")

View file

@ -34,7 +34,7 @@ class Document(Base):
__tablename__ = "documents"
id: Mapped[str] = mapped_column(Text, primary_key=True)
agent_id: Mapped[str] = mapped_column(Text, primary_key=True)
bank_id: Mapped[str] = mapped_column(Text, primary_key=True)
original_text: Mapped[Optional[str]] = mapped_column(Text)
content_hash: Mapped[Optional[str]] = mapped_column(Text)
doc_metadata: Mapped[dict] = mapped_column("metadata", JSONB, server_default=sql_text("'{}'::jsonb"))
@ -49,7 +49,7 @@ class Document(Base):
memory_units = relationship("MemoryUnit", back_populates="document", cascade="all, delete-orphan")
__table_args__ = (
Index("idx_documents_agent_id", "agent_id"),
Index("idx_documents_bank_id", "bank_id"),
Index("idx_documents_content_hash", "content_hash"),
)
@ -61,7 +61,7 @@ class MemoryUnit(Base):
id: Mapped[PyUUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, server_default=sql_text("uuid_generate_v4()")
)
agent_id: Mapped[str] = mapped_column(Text, nullable=False)
bank_id: Mapped[str] = mapped_column(Text, nullable=False)
document_id: Mapped[Optional[str]] = mapped_column(Text)
text: Mapped[str] = mapped_column(Text, nullable=False)
embedding = mapped_column(Vector(384)) # pgvector type
@ -99,12 +99,12 @@ class MemoryUnit(Base):
__table_args__ = (
ForeignKeyConstraint(
["document_id", "agent_id"],
["documents.id", "documents.agent_id"],
["document_id", "bank_id"],
["documents.id", "documents.bank_id"],
name="memory_units_document_fkey",
ondelete="CASCADE",
),
CheckConstraint("fact_type IN ('world', 'agent', 'opinion', 'observation')"),
CheckConstraint("fact_type IN ('world', 'bank', 'opinion', 'observation')"),
CheckConstraint("confidence_score IS NULL OR (confidence_score >= 0.0 AND confidence_score <= 1.0)"),
CheckConstraint(
"(fact_type = 'opinion' AND confidence_score IS NOT NULL) OR "
@ -112,31 +112,31 @@ class MemoryUnit(Base):
"(fact_type NOT IN ('opinion', 'observation') AND confidence_score IS NULL)",
name="confidence_score_fact_type_check"
),
Index("idx_memory_units_agent_id", "agent_id"),
Index("idx_memory_units_bank_id", "bank_id"),
Index("idx_memory_units_document_id", "document_id"),
Index("idx_memory_units_event_date", "event_date", postgresql_ops={"event_date": "DESC"}),
Index("idx_memory_units_agent_date", "agent_id", "event_date", postgresql_ops={"event_date": "DESC"}),
Index("idx_memory_units_bank_date", "bank_id", "event_date", postgresql_ops={"event_date": "DESC"}),
Index("idx_memory_units_access_count", "access_count", postgresql_ops={"access_count": "DESC"}),
Index("idx_memory_units_fact_type", "fact_type"),
Index("idx_memory_units_agent_fact_type", "agent_id", "fact_type"),
Index("idx_memory_units_agent_type_date", "agent_id", "fact_type", "event_date", postgresql_ops={"event_date": "DESC"}),
Index("idx_memory_units_bank_fact_type", "bank_id", "fact_type"),
Index("idx_memory_units_bank_type_date", "bank_id", "fact_type", "event_date", postgresql_ops={"event_date": "DESC"}),
Index(
"idx_memory_units_opinion_confidence",
"agent_id",
"bank_id",
"confidence_score",
postgresql_where=sql_text("fact_type = 'opinion'"),
postgresql_ops={"confidence_score": "DESC"}
),
Index(
"idx_memory_units_opinion_date",
"agent_id",
"bank_id",
"event_date",
postgresql_where=sql_text("fact_type = 'opinion'"),
postgresql_ops={"event_date": "DESC"}
),
Index(
"idx_memory_units_observation_date",
"agent_id",
"bank_id",
"event_date",
postgresql_where=sql_text("fact_type = 'observation'"),
postgresql_ops={"event_date": "DESC"}
@ -158,7 +158,7 @@ class Entity(Base):
UUID(as_uuid=True), primary_key=True, server_default=sql_text("uuid_generate_v4()")
)
canonical_name: Mapped[str] = mapped_column(Text, nullable=False)
agent_id: Mapped[str] = mapped_column(Text, nullable=False)
bank_id: Mapped[str] = mapped_column(Text, nullable=False)
entity_metadata: Mapped[dict] = mapped_column("metadata", JSONB, server_default=sql_text("'{}'::jsonb"))
first_seen: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
@ -185,9 +185,9 @@ class Entity(Base):
)
__table_args__ = (
Index("idx_entities_agent_id", "agent_id"),
Index("idx_entities_bank_id", "bank_id"),
Index("idx_entities_canonical_name", "canonical_name"),
Index("idx_entities_agent_name", "agent_id", "canonical_name"),
Index("idx_entities_bank_name", "bank_id", "canonical_name"),
)
@ -264,6 +264,11 @@ class MemoryLink(Base):
entity = relationship("Entity", back_populates="memory_links")
__table_args__ = (
CheckConstraint(
"link_type IN ('temporal', 'semantic', 'entity', 'causes', 'caused_by', 'enables', 'prevents')",
name="memory_links_link_type_check"
),
CheckConstraint("weight >= 0.0 AND weight <= 1.0", name="memory_links_weight_check"),
Index("idx_memory_links_from", "from_unit_id"),
Index("idx_memory_links_to", "to_unit_id"),
Index("idx_memory_links_type", "link_type"),
@ -278,11 +283,11 @@ class MemoryLink(Base):
)
class Agent(Base):
"""Agent profiles with personality traits and background."""
__tablename__ = "agents"
class Bank(Base):
"""Memory bank profiles with personality traits and background."""
__tablename__ = "banks"
agent_id: Mapped[str] = mapped_column(Text, primary_key=True)
bank_id: Mapped[str] = mapped_column(Text, primary_key=True)
personality: Mapped[dict] = mapped_column(
JSONB,
nullable=False,
@ -300,5 +305,5 @@ class Agent(Base):
)
__table_args__ = (
Index("idx_agents_agent_id", "agent_id"),
Index("idx_banks_bank_id", "bank_id"),
)

View file

@ -111,6 +111,7 @@ if __name__ == "__main__":
"log_level": args.log_level,
"access_log": args.access_log,
"proxy_headers": args.proxy_headers,
"ws": "wsproto", # Use wsproto instead of websockets to avoid deprecation warnings
}
# Add optional parameters if provided

View file

@ -18,6 +18,7 @@ dependencies = [
"langchain-text-splitters>=0.3.0",
"fastapi[standard]>=0.120.3",
"uvicorn>=0.38.0",
"wsproto>=1.0.0",
"sqlalchemy>=2.0.44",
"alembic>=1.17.1",
"pgvector>=0.4.1",

View file

@ -4,7 +4,8 @@ Tests for agent management API (profile, personality, background).
import pytest
import uuid
from hindsight_api import MemoryEngine
from hindsight_api.api import CreateAgentRequest, PersonalityTraits
from hindsight_api.api import CreateBankRequest, PersonalityTraits
from hindsight_api.engine.memory_engine import Budget
def unique_agent_id(prefix: str) -> str:
@ -18,9 +19,9 @@ class TestAgentProfile:
@pytest.mark.asyncio
async def test_get_agent_profile_creates_default(self, memory: MemoryEngine):
"""Test that getting a profile for a new agent creates default personality."""
agent_id = unique_agent_id("test_profile_default")
bank_id = unique_agent_id("test_profile_default")
profile = await memory.get_agent_profile(agent_id)
profile = await memory.get_bank_profile(bank_id)
assert profile is not None
assert "personality" in profile
@ -39,9 +40,9 @@ class TestAgentProfile:
@pytest.mark.asyncio
async def test_update_agent_personality(self, memory: MemoryEngine):
"""Test updating agent personality traits."""
agent_id = unique_agent_id("test_profile_update")
bank_id = unique_agent_id("test_profile_update")
profile = await memory.get_agent_profile(agent_id)
profile = await memory.get_bank_profile(bank_id)
assert profile["personality"]["openness"] == 0.5
new_personality = {
@ -52,9 +53,9 @@ class TestAgentProfile:
"neuroticism": 0.3,
"bias_strength": 0.9,
}
await memory.update_agent_personality(agent_id, new_personality)
await memory.update_bank_personality(bank_id, new_personality)
updated_profile = await memory.get_agent_profile(agent_id)
updated_profile = await memory.get_bank_profile(bank_id)
for key in new_personality:
assert abs(updated_profile["personality"][key] - new_personality[key]) < 0.001
@ -65,19 +66,19 @@ class TestAgentProfile:
agent_id_2 = unique_agent_id("test_list")
agent_id_3 = unique_agent_id("test_list")
await memory.get_agent_profile(agent_id_1)
await memory.get_agent_profile(agent_id_2)
await memory.get_agent_profile(agent_id_3)
await memory.get_bank_profile(agent_id_1)
await memory.get_bank_profile(agent_id_2)
await memory.get_bank_profile(agent_id_3)
agents = await memory.list_agents()
agents = await memory.list_banks()
agent_ids = [a["agent_id"] for a in agents]
agent_ids = [a["bank_id"] for a in agents]
assert agent_id_1 in agent_ids
assert agent_id_2 in agent_ids
assert agent_id_3 in agent_ids
for agent in agents:
assert "agent_id" in agent
assert "bank_id" in agent
assert "personality" in agent
assert "background" in agent
assert "created_at" in agent
@ -90,42 +91,42 @@ class TestAgentBackground:
@pytest.mark.asyncio
async def test_merge_agent_background(self, memory: MemoryEngine):
"""Test merging agent background information."""
agent_id = unique_agent_id("test_profile_merge")
bank_id = unique_agent_id("test_profile_merge")
profile = await memory.get_agent_profile(agent_id)
profile = await memory.get_bank_profile(bank_id)
assert profile["background"] == ""
result1 = await memory.merge_agent_background(
agent_id,
result1 = await memory.merge_bank_background(
bank_id,
"I was born in Texas",
update_personality=False
)
assert "Texas" in result1["background"]
result2 = await memory.merge_agent_background(
agent_id,
result2 = await memory.merge_bank_background(
bank_id,
"I have 10 years of startup experience",
update_personality=False
)
assert "Texas" in result2["background"] or "startup" in result2["background"]
final_profile = await memory.get_agent_profile(agent_id)
final_profile = await memory.get_bank_profile(bank_id)
assert final_profile["background"] != ""
@pytest.mark.asyncio
async def test_merge_background_handles_conflicts(self, memory: MemoryEngine):
"""Test that merging background handles conflicts (new overwrites old)."""
agent_id = unique_agent_id("test_profile_conflict")
bank_id = unique_agent_id("test_profile_conflict")
result1 = await memory.merge_agent_background(
agent_id,
result1 = await memory.merge_bank_background(
bank_id,
"I was born in Colorado",
update_personality=False
)
assert "Colorado" in result1["background"]
result2 = await memory.merge_agent_background(
agent_id,
result2 = await memory.merge_bank_background(
bank_id,
"You were born in Texas",
update_personality=False
)
@ -138,9 +139,9 @@ class TestAgentEndpoint:
@pytest.mark.asyncio
async def test_put_agent_create(self, memory: MemoryEngine):
"""Test creating an agent via PUT endpoint."""
agent_id = unique_agent_id("test_put_create")
bank_id = unique_agent_id("test_put_create")
request = CreateAgentRequest(
request = CreateBankRequest(
personality=PersonalityTraits(
openness=0.8,
conscientiousness=0.6,
@ -152,11 +153,11 @@ class TestAgentEndpoint:
background="I am a creative software engineer"
)
profile = await memory.get_agent_profile(agent_id)
profile = await memory.get_bank_profile(bank_id)
if request.personality is not None:
await memory.update_agent_personality(
agent_id,
await memory.update_bank_personality(
bank_id,
request.personality.model_dump()
)
@ -165,16 +166,16 @@ class TestAgentEndpoint:
async with pool.acquire() as conn:
await conn.execute(
"""
UPDATE agents
UPDATE banks
SET background = $2,
updated_at = NOW()
WHERE agent_id = $1
WHERE bank_id = $1
""",
agent_id,
bank_id,
request.background
)
final_profile = await memory.get_agent_profile(agent_id)
final_profile = await memory.get_bank_profile(bank_id)
assert final_profile["personality"]["openness"] == 0.8
assert final_profile["personality"]["bias_strength"] == 0.7
@ -183,29 +184,29 @@ class TestAgentEndpoint:
@pytest.mark.asyncio
async def test_put_agent_partial_update(self, memory: MemoryEngine):
"""Test updating only background."""
agent_id = unique_agent_id("test_put_partial")
bank_id = unique_agent_id("test_put_partial")
request = CreateAgentRequest(
request = CreateBankRequest(
background="I am a data scientist"
)
profile = await memory.get_agent_profile(agent_id)
profile = await memory.get_bank_profile(bank_id)
if request.background is not None:
pool = await memory._get_pool()
async with pool.acquire() as conn:
await conn.execute(
"""
UPDATE agents
UPDATE banks
SET background = $2,
updated_at = NOW()
WHERE agent_id = $1
WHERE bank_id = $1
""",
agent_id,
bank_id,
request.background
)
final_profile = await memory.get_agent_profile(agent_id)
final_profile = await memory.get_bank_profile(bank_id)
assert final_profile["personality"]["openness"] == 0.5
assert final_profile["background"] == "I am a data scientist"
@ -217,7 +218,7 @@ class TestAgentPersonalityIntegration:
@pytest.mark.asyncio
async def test_think_uses_personality(self, memory: MemoryEngine):
"""Test that THINK operation uses agent personality."""
agent_id = unique_agent_id("test_think")
bank_id = unique_agent_id("test_think")
personality = {
"openness": 0.9,
@ -227,16 +228,16 @@ class TestAgentPersonalityIntegration:
"neuroticism": 0.7,
"bias_strength": 0.9,
}
await memory.update_agent_personality(agent_id, personality)
await memory.update_bank_personality(bank_id, personality)
await memory.merge_agent_background(
agent_id,
await memory.merge_bank_background(
bank_id,
"I am a creative artist who values innovation over tradition",
update_personality=False
)
await memory.put_batch_async(
agent_id=agent_id,
await memory.retain_batch_async(
bank_id=bank_id,
contents=[
{"content": "Traditional painting techniques have been used for centuries"},
{"content": "Modern digital art is changing the art world"}
@ -244,10 +245,10 @@ class TestAgentPersonalityIntegration:
document_id="art_facts"
)
result = await memory.think_async(
agent_id=agent_id,
result = await memory.reflect_async(
bank_id=bank_id,
query="What do you think about traditional vs modern art?",
thinking_budget=50
budget=Budget.LOW
)
assert result.text is not None

View file

@ -7,7 +7,7 @@ import os
@pytest.mark.asyncio
async def test_large_batch_auto_chunks(memory):
agent_id = "test_chunking_agent"
bank_id = "test_chunking_agent"
# Create a large batch that should trigger chunking
# Each item is ~2000 chars, so 30 items = 60k chars (exceeds 50k threshold)
large_content = "Alice met with Bob at the coffee shop. " * 50 # ~2000 chars
@ -22,8 +22,8 @@ async def test_large_batch_auto_chunks(memory):
print(f"Should trigger chunking: {total_chars > 50_000}")
# Ingest the large batch (should auto-chunk)
result = await memory.put_batch_async(
agent_id=agent_id,
result = await memory.retain_batch_async(
bank_id=bank_id,
contents=contents
)
@ -34,7 +34,7 @@ async def test_large_batch_auto_chunks(memory):
@pytest.mark.asyncio
async def test_small_batch_no_chunking(memory):
agent_id = "test_no_chunking_agent"
bank_id = "test_no_chunking_agent"
# Create a small batch that should NOT trigger chunking
contents = [
@ -48,8 +48,8 @@ async def test_small_batch_no_chunking(memory):
print(f"Should NOT trigger chunking: {total_chars <= 50_000}")
# Ingest the small batch (should NOT auto-chunk)
result = await memory.put_batch_async(
agent_id=agent_id,
result = await memory.retain_batch_async(
bank_id=bank_id,
contents=contents
)

View file

@ -9,62 +9,62 @@ from datetime import datetime, timezone
@pytest.mark.asyncio
async def test_document_creation_and_retrieval(memory):
"""Test that documents are created and can be retrieved."""
agent_id = f"test_doc_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_doc_{datetime.now(timezone.utc).timestamp()}"
try:
document_id = "meeting-001"
# Store memory with document tracking
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Alice works at Google. Bob works at Microsoft.",
context="Team meeting",
document_id=document_id
)
# Retrieve document
doc = await memory.get_document(document_id, agent_id)
doc = await memory.get_document(document_id, bank_id)
assert doc is not None
assert doc["id"] == document_id
assert doc["agent_id"] == agent_id
assert doc["bank_id"] == bank_id
assert "Alice works at Google" in doc["original_text"]
assert doc["memory_unit_count"] > 0
finally:
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)
@pytest.mark.asyncio
async def test_document_upsert(memory):
"""Test that providing the same document_id automatically upserts (deletes old units and creates new ones)."""
agent_id = f"test_upsert_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_upsert_{datetime.now(timezone.utc).timestamp()}"
try:
document_id = "meeting-002"
# First version
units_v1 = await memory.put_async(
agent_id=agent_id,
units_v1 = await memory.retain_async(
bank_id=bank_id,
content="Alice works at Google.",
context="Initial",
document_id=document_id
)
# Get document stats
doc_v1 = await memory.get_document(document_id, agent_id)
doc_v1 = await memory.get_document(document_id, bank_id)
count_v1 = doc_v1["memory_unit_count"]
# Update with different content (automatic upsert when same document_id is provided)
units_v2 = await memory.put_async(
agent_id=agent_id,
units_v2 = await memory.retain_async(
bank_id=bank_id,
content="Alice works at Microsoft. Bob works at Apple.",
context="Updated",
document_id=document_id
)
# Get updated document stats
doc_v2 = await memory.get_document(document_id, agent_id)
doc_v2 = await memory.get_document(document_id, bank_id)
count_v2 = doc_v2["memory_unit_count"]
# Verify old units were replaced
@ -75,52 +75,52 @@ async def test_document_upsert(memory):
assert set(units_v1).isdisjoint(set(units_v2))
finally:
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)
@pytest.mark.asyncio
async def test_document_deletion(memory):
"""Test that deleting a document cascades to memory units."""
agent_id = f"test_delete_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_delete_{datetime.now(timezone.utc).timestamp()}"
try:
document_id = "meeting-003"
# Create document
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Alice works at Google.",
context="Test",
document_id=document_id
)
# Verify it exists
doc = await memory.get_document(document_id, agent_id)
doc = await memory.get_document(document_id, bank_id)
assert doc is not None
assert doc["memory_unit_count"] > 0
# Delete document
result = await memory.delete_document(document_id, agent_id)
result = await memory.delete_document(document_id, bank_id)
assert result["document_deleted"] == 1
assert result["memory_units_deleted"] > 0
# Verify it's gone
doc_after = await memory.get_document(document_id, agent_id)
doc_after = await memory.get_document(document_id, bank_id)
assert doc_after is None
finally:
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)
@pytest.mark.asyncio
async def test_memory_without_document(memory):
"""Test that memories can still be created without document tracking."""
agent_id = f"test_no_doc_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_no_doc_{datetime.now(timezone.utc).timestamp()}"
try:
# Create memory without document_id (backward compatibility)
units = await memory.put_async(
agent_id=agent_id,
units = await memory.retain_async(
bank_id=bank_id,
content="Alice works at Google.",
context="Test"
)
@ -128,4 +128,4 @@ async def test_memory_without_document(memory):
assert len(units) > 0
finally:
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)

View file

@ -773,7 +773,7 @@ class TestFactClassification:
This test addresses the issue where podcast transcripts with context like
"this was podcast episode between you (Marcus) and Jamie" were extracting
all facts as 'world' instead of properly identifying Marcus's statements as 'agent'.
all facts as 'world' instead of properly identifying Marcus's statements as 'bank'.
"""
transcript = """
@ -808,7 +808,7 @@ Jamie: Congratulations! I'd love to read it.
agent_facts = [f for f in facts if f["fact_type"] == "agent"]
assert len(agent_facts) > 0, \
f"Should have at least one 'agent' fact when context identifies 'you (Marcus)'. " \
f"Should have at least one 'bank' fact when context identifies 'you (Marcus)'. " \
f"Got facts: {[f['fact'] + ' [' + f['fact_type'] + ']' for f in facts]}"
for agent_fact in agent_facts:
@ -1019,10 +1019,10 @@ class TestPersonalityInference:
async def test_background_merge_with_personality_inference(self, memory):
"""Test that background merge infers personality traits by default."""
import uuid
agent_id = f"test_infer_{uuid.uuid4().hex[:8]}"
bank_id = f"test_infer_{uuid.uuid4().hex[:8]}"
result = await memory.merge_agent_background(
agent_id,
result = await memory.merge_bank_background(
bank_id,
"I am a creative software engineer who loves innovation and trying new technologies",
update_personality=True
)
@ -1049,13 +1049,13 @@ class TestPersonalityInference:
async def test_background_merge_without_personality_inference(self, memory):
"""Test that background merge skips personality inference when disabled."""
import uuid
agent_id = f"test_no_infer_{uuid.uuid4().hex[:8]}"
bank_id = f"test_no_infer_{uuid.uuid4().hex[:8]}"
initial_profile = await memory.get_agent_profile(agent_id)
initial_profile = await memory.get_bank_profile(bank_id)
initial_personality = initial_profile["personality"]
result = await memory.merge_agent_background(
agent_id,
result = await memory.merge_bank_background(
bank_id,
"I am a data scientist",
update_personality=False
)
@ -1063,7 +1063,7 @@ class TestPersonalityInference:
assert "background" in result
assert "personality" not in result
final_profile = await memory.get_agent_profile(agent_id)
final_profile = await memory.get_bank_profile(bank_id)
final_personality = final_profile["personality"]
assert initial_personality == final_personality
@ -1072,10 +1072,10 @@ class TestPersonalityInference:
async def test_personality_inference_for_organized_engineer(self, memory):
"""Test personality inference for organized/conscientious profile."""
import uuid
agent_id = f"test_organized_{uuid.uuid4().hex[:8]}"
bank_id = f"test_organized_{uuid.uuid4().hex[:8]}"
result = await memory.merge_agent_background(
agent_id,
result = await memory.merge_bank_background(
bank_id,
"I am a methodical engineer who values organization and systematic planning",
update_personality=True
)
@ -1088,10 +1088,10 @@ class TestPersonalityInference:
async def test_personality_inference_for_startup_founder(self, memory):
"""Test personality inference for entrepreneurial profile."""
import uuid
agent_id = f"test_founder_{uuid.uuid4().hex[:8]}"
bank_id = f"test_founder_{uuid.uuid4().hex[:8]}"
result = await memory.merge_agent_background(
agent_id,
result = await memory.merge_bank_background(
bank_id,
"I am a startup founder who thrives on risk and social interaction",
update_personality=True
)
@ -1105,17 +1105,17 @@ class TestPersonalityInference:
async def test_personality_updates_in_database(self, memory):
"""Test that inferred personality is actually stored in database."""
import uuid
agent_id = f"test_db_update_{uuid.uuid4().hex[:8]}"
bank_id = f"test_db_update_{uuid.uuid4().hex[:8]}"
result = await memory.merge_agent_background(
agent_id,
result = await memory.merge_bank_background(
bank_id,
"I am an innovative designer",
update_personality=True
)
inferred_personality = result["personality"]
profile = await memory.get_agent_profile(agent_id)
profile = await memory.get_bank_profile(bank_id)
db_personality = profile["personality"]
assert db_personality == inferred_personality
@ -1124,17 +1124,17 @@ class TestPersonalityInference:
async def test_multiple_background_merges_update_personality(self, memory):
"""Test that each background merge can update personality."""
import uuid
agent_id = f"test_multi_merge_{uuid.uuid4().hex[:8]}"
bank_id = f"test_multi_merge_{uuid.uuid4().hex[:8]}"
result1 = await memory.merge_agent_background(
agent_id,
result1 = await memory.merge_bank_background(
bank_id,
"I am a software engineer",
update_personality=True
)
personality1 = result1["personality"]
result2 = await memory.merge_agent_background(
agent_id,
result2 = await memory.merge_bank_background(
bank_id,
"I love creative problem solving and innovation",
update_personality=True
)
@ -1147,16 +1147,16 @@ class TestPersonalityInference:
async def test_background_merge_conflict_resolution_with_personality(self, memory):
"""Test that conflicts are resolved and personality reflects final background."""
import uuid
agent_id = f"test_conflict_{uuid.uuid4().hex[:8]}"
bank_id = f"test_conflict_{uuid.uuid4().hex[:8]}"
await memory.merge_agent_background(
agent_id,
await memory.merge_bank_background(
bank_id,
"I was born in Colorado and prefer stability",
update_personality=True
)
result = await memory.merge_agent_background(
agent_id,
result = await memory.merge_bank_background(
bank_id,
"You were born in Texas and love taking risks",
update_personality=True
)

View file

@ -8,18 +8,19 @@ distinguish between things said earlier vs later.
import pytest
from datetime import datetime, timezone
from hindsight_api import MemoryEngine
from hindsight_api.engine.memory_engine import Budget
import os
@pytest.mark.asyncio
async def test_fact_ordering_within_conversation(memory):
agent_id = "test_ordering_agent"
bank_id = "test_ordering_agent"
# Get/create agent (auto-creates with defaults)
await memory.get_agent_profile(agent_id)
await memory.get_bank_profile(bank_id)
# Update personality to match Marcus
await memory.update_agent_personality(agent_id, {
await memory.update_bank_personality(bank_id, {
"openness": 0.7,
"conscientiousness": 0.6,
"extraversion": 0.8,
@ -40,8 +41,8 @@ Marcus: Yeah, I realized I was being too optimistic about their defense.
base_event_date = datetime(2024, 11, 14, 10, 0, 0, tzinfo=timezone.utc)
# Store the conversation
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content=conversation,
context="podcast discussion about NFL game",
event_date=base_event_date,
@ -49,28 +50,28 @@ Marcus: Yeah, I realized I was being too optimistic about their defense.
)
# Search for all facts about Marcus's predictions
results = await memory.search_async(
agent_id=agent_id,
results = await memory.recall_async(
bank_id=bank_id,
query="Marcus prediction Rams",
fact_type=['agent', 'world'],
thinking_budget=100,
fact_type=['bank', 'world'],
budget=Budget.LOW,
max_tokens=8192
)
print(f"\n=== Retrieved {len(results.results)} facts ===")
for i, result in enumerate(results.results):
print(f"{i+1}. [{result.event_date}] {result.text[:100]}")
print(f"{i+1}. [{result.mentioned_at}] {result.text[:100]}")
# Get all agent facts (Marcus's statements)
agent_facts = [r for r in results.results if r.fact_type == 'agent']
agent_facts = [r for r in results.results if r.fact_type == 'bank']
print(f"\n=== Agent facts (Marcus's statements) ===")
for i, fact in enumerate(agent_facts):
print(f"{i+1}. [{fact.event_date}] {fact.text}")
print(f"{i+1}. [{fact.mentioned_at}] {fact.text}")
# Check that agent facts have different timestamps
if len(agent_facts) >= 2:
timestamps = [datetime.fromisoformat(f.event_date.replace('Z', '+00:00')) for f in agent_facts]
timestamps = [datetime.fromisoformat(f.mentioned_at.replace('Z', '+00:00')) for f in agent_facts]
# Verify timestamps are different (have time offsets)
unique_timestamps = set(timestamps)
@ -115,7 +116,7 @@ Marcus: Yeah, I realized I was being too optimistic about their defense.
print(f"\n✅ Temporal ordering preserved: First prediction came before changed prediction")
# Cleanup
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)
print(f"\n✅ Test passed: Fact ordering within conversation is preserved")
@ -123,9 +124,9 @@ Marcus: Yeah, I realized I was being too optimistic about their defense.
@pytest.mark.asyncio
async def test_multiple_documents_ordering(memory):
agent_id = "test_multi_doc_agent"
bank_id = "test_multi_doc_agent"
await memory.get_agent_profile(agent_id) # Auto-creates with defaults
await memory.get_bank_profile(bank_id) # Auto-creates with defaults
# Two separate conversations with same base time
base_time = datetime(2024, 11, 14, 10, 0, 0, tzinfo=timezone.utc)
@ -143,8 +144,8 @@ Alice: I reconsidered the team's experience level.
"""
# Store both conversations with batch
await memory.put_batch_async(
agent_id=agent_id,
await memory.retain_batch_async(
bank_id=bank_id,
contents=[
{"content": conv1, "context": "project discussion 1", "event_date": base_time},
{"content": conv2, "context": "project discussion 2", "event_date": base_time}
@ -152,23 +153,23 @@ Alice: I reconsidered the team's experience level.
)
# Search for Alice's preferences
results = await memory.search_async(
agent_id=agent_id,
results = await memory.recall_async(
bank_id=bank_id,
query="Alice preference React Vue",
fact_type=['agent'],
thinking_budget=100,
fact_type=['bank'],
budget=Budget.LOW,
max_tokens=8192
)
print(f"\n=== Retrieved {len(results.results)} agent facts ===")
agent_facts = [r for r in results.results if r.fact_type == 'agent']
agent_facts = [r for r in results.results if r.fact_type == 'bank']
for i, fact in enumerate(agent_facts):
print(f"{i+1}. [{fact.event_date}] {fact.text[:80]}")
print(f"{i+1}. [{fact.mentioned_at}] {fact.text[:80]}")
# Each conversation's facts should have different timestamps
if len(agent_facts) >= 2:
timestamps = [datetime.fromisoformat(f.event_date.replace('Z', '+00:00')) for f in agent_facts]
timestamps = [datetime.fromisoformat(f.mentioned_at.replace('Z', '+00:00')) for f in agent_facts]
unique_timestamps = set(timestamps)
assert len(unique_timestamps) >= 2, \
@ -177,6 +178,6 @@ Alice: I reconsidered the team's experience level.
print(f"\n✅ Facts from {len(agent_facts)} statements have {len(unique_timestamps)} unique timestamps")
# Cleanup
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)
print(f"\n✅ Test passed: Multiple documents maintain separate ordering")

View file

@ -21,50 +21,52 @@ async def api_client(memory):
@pytest.fixture
def test_agent_id():
"""Provide a unique agent ID for this test run."""
def test_bank_id():
"""Provide a unique bank ID for this test run."""
return f"integration_test_{datetime.now().timestamp()}"
@pytest.mark.asyncio
async def test_full_api_workflow(api_client, test_agent_id):
async def test_full_api_workflow(api_client, test_bank_id):
"""
End-to-end test covering all major API endpoints in a realistic workflow.
Workflow:
1. Create agent and set profile
2. Store memories (put, batch put)
3. Search memories
4. Think (generate answer)
5. List agents and memories
6. Get agent profile
1. Create bank and set profile
2. Store memories (retain)
3. Recall memories
4. Reflect (generate answer)
5. List banks and memories
6. Get bank profile
7. Get visualization data
8. Track documents
9. Clean up
9. Test entity endpoints
10. Test operations endpoints
11. Clean up
"""
# ================================================================
# 1. Agent Management
# 1. Bank Management
# ================================================================
# List agents (should be empty initially or have other test agents)
response = await api_client.get("/api/v1/agents")
# List banks (should be empty initially or have other test banks)
response = await api_client.get("/v1/default/banks")
assert response.status_code == 200
initial_agents_data = response.json()["agents"]
initial_agents = [a["agent_id"] for a in initial_agents_data]
print(f"Initial agents: {len(initial_agents)}")
initial_banks_data = response.json()["banks"]
initial_banks = [a["bank_id"] for a in initial_banks_data]
print(f"Initial banks: {len(initial_banks)}")
# Get agent profile (creates default if not exists)
response = await api_client.get(f"/api/v1/agents/{test_agent_id}/profile")
# Get bank profile (creates default if not exists)
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
assert response.status_code == 200
profile = response.json()
assert "personality" in profile
assert "background" in profile
print(f"Agent profile created with personality: {profile['personality']}")
print(f"Bank profile created with personality: {profile['personality']}")
# Add background
response = await api_client.post(
f"/api/v1/agents/{test_agent_id}/background",
f"/v1/default/banks/{test_bank_id}/background",
json={
"content": "A software engineer passionate about AI and memory systems."
}
@ -79,7 +81,7 @@ async def test_full_api_workflow(api_client, test_agent_id):
# Store single memory (using batch endpoint with single item)
response = await api_client.post(
f"/api/v1/agents/{test_agent_id}/memories",
f"/v1/default/banks/{test_bank_id}/memories",
json={
"items": [
{
@ -97,7 +99,7 @@ async def test_full_api_workflow(api_client, test_agent_id):
# Store batch memories
response = await api_client.post(
f"/api/v1/agents/{test_agent_id}/memories",
f"/v1/default/banks/{test_bank_id}/memories",
json={
"items": [
{
@ -118,12 +120,12 @@ async def test_full_api_workflow(api_client, test_agent_id):
print(f"Stored {batch_result['items_count']} items from batch put")
# ================================================================
# 3. Search
# 3. Recall (Search)
# ================================================================
# Search for memories
# Recall memories
response = await api_client.post(
f"/api/v1/agents/{test_agent_id}/memories/search",
f"/v1/default/banks/{test_bank_id}/memories/recall",
json={
"query": "Who works on machine learning?",
"thinking_budget": 50
@ -140,12 +142,12 @@ async def test_full_api_workflow(api_client, test_agent_id):
assert found_alice, "Should find Alice in search results"
# ================================================================
# 4. Think (Reasoning)
# 4. Reflect (Reasoning)
# ================================================================
# Generate answer using think
# Generate answer using reflect
response = await api_client.post(
f"/api/v1/agents/{test_agent_id}/think",
f"/v1/default/banks/{test_bank_id}/reflect",
json={
"query": "What do you know about the team members?",
"thinking_budget": 30,
@ -153,14 +155,14 @@ async def test_full_api_workflow(api_client, test_agent_id):
}
)
assert response.status_code == 200
think_result = response.json()
assert "text" in think_result
assert len(think_result["text"]) > 0
assert "based_on" in think_result
print(f"Think response: {think_result['text'][:100]}...")
reflect_result = response.json()
assert "text" in reflect_result
assert len(reflect_result["text"]) > 0
assert "based_on" in reflect_result
print(f"Reflect response: {reflect_result['text'][:100]}...")
# Verify the answer mentions team members
answer = think_result["text"].lower()
answer = reflect_result["text"].lower()
assert "alice" in answer or "bob" in answer or "charlie" in answer
# ================================================================
@ -168,7 +170,7 @@ async def test_full_api_workflow(api_client, test_agent_id):
# ================================================================
# Get graph data
response = await api_client.get(f"/api/v1/agents/{test_agent_id}/graph")
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/graph")
assert response.status_code == 200
graph_data = response.json()
assert "nodes" in graph_data
@ -176,7 +178,7 @@ async def test_full_api_workflow(api_client, test_agent_id):
print(f"Graph has {len(graph_data['nodes'])} nodes and {len(graph_data['edges'])} edges")
# Get memory statistics
response = await api_client.get(f"/api/v1/agents/{test_agent_id}/stats")
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/stats")
assert response.status_code == 200
stats = response.json()
assert "total_nodes" in stats
@ -185,7 +187,7 @@ async def test_full_api_workflow(api_client, test_agent_id):
# List memory units
response = await api_client.get(
f"/api/v1/agents/{test_agent_id}/memories/list",
f"/v1/default/banks/{test_bank_id}/memories/list",
params={"limit": 10}
)
assert response.status_code == 200
@ -200,7 +202,7 @@ async def test_full_api_workflow(api_client, test_agent_id):
# Store memory with document
response = await api_client.post(
f"/api/v1/agents/{test_agent_id}/memories",
f"/v1/default/banks/{test_bank_id}/memories",
json={
"items": [
{
@ -215,7 +217,7 @@ async def test_full_api_workflow(api_client, test_agent_id):
print("Stored memory with document tracking")
# List documents
response = await api_client.get(f"/api/v1/agents/{test_agent_id}/documents")
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/documents")
assert response.status_code == 200
documents = response.json()
assert "items" in documents
@ -224,7 +226,7 @@ async def test_full_api_workflow(api_client, test_agent_id):
# Get specific document
response = await api_client.get(
f"/api/v1/agents/{test_agent_id}/documents/roadmap-2024-q1"
f"/v1/default/banks/{test_bank_id}/documents/roadmap-2024-q1"
)
assert response.status_code == 200
doc_info = response.json()
@ -235,35 +237,81 @@ async def test_full_api_workflow(api_client, test_agent_id):
# Note: Document deletion is tested separately in test_document_deletion
# ================================================================
# 7. Verify Updated Agent Profile
# 7. Update and Verify Bank Personality
# ================================================================
# Check profile again (might have formed new opinions)
response = await api_client.get(f"/api/v1/agents/{test_agent_id}/profile")
# Update personality traits
response = await api_client.put(
f"/v1/default/banks/{test_bank_id}/profile",
json={
"personality": {
"openness": 0.8,
"conscientiousness": 0.7,
"extraversion": 0.6,
"agreeableness": 0.9,
"neuroticism": 0.3,
"bias_strength": 0.5
}
}
)
assert response.status_code == 200
print("Personality updated")
# Check profile again (should have updated personality)
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
assert response.status_code == 200
updated_profile = response.json()
assert "software engineer" in updated_profile["background"].lower()
print("Profile verified")
# ================================================================
# 8. List All Agents (should include our test agent)
# 8. Test Entity Endpoints
# ================================================================
response = await api_client.get("/api/v1/agents")
# List entities
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/entities")
assert response.status_code == 200
final_agents_data = response.json()["agents"]
final_agents = [a["agent_id"] for a in final_agents_data]
assert test_agent_id in final_agents
assert len(final_agents) >= len(initial_agents) + 1
print(f"Final agent count: {len(final_agents)}")
entities_data = response.json()
assert "items" in entities_data
print(f"Found {len(entities_data['items'])} entities")
# Get specific entity if any exist
if len(entities_data['items']) > 0:
entity_id = entities_data['items'][0]['id']
response = await api_client.get(
f"/v1/default/banks/{test_bank_id}/entities/{entity_id}"
)
assert response.status_code == 200
entity_detail = response.json()
assert "id" in entity_detail
print(f"Retrieved entity: {entity_detail.get('name', entity_id)}")
# Test regenerate observations
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/entities/{entity_id}/regenerate"
)
assert response.status_code == 200
print(f"Regenerated observations for entity {entity_id}")
# ================================================================
# 9. Clean Up
# 9. List All Banks (should include our test bank)
# ================================================================
# Note: No delete agent endpoint in API, so test data remains in DB
# Using timestamped agent IDs prevents conflicts between test runs
print(f"Integration test complete for agent {test_agent_id}")
response = await api_client.get("/v1/default/banks")
assert response.status_code == 200
final_banks_data = response.json()["banks"]
final_banks = [a["bank_id"] for a in final_banks_data]
assert test_bank_id in final_banks
assert len(final_banks) >= len(initial_banks) + 1
print(f"Final bank count: {len(final_banks)}")
# ================================================================
# 10. Clean Up
# ================================================================
# Note: No delete bank endpoint in API, so test data remains in DB
# Using timestamped bank IDs prevents conflicts between test runs
print(f"Integration test complete for bank {test_bank_id}")
@pytest.mark.asyncio
@ -272,7 +320,7 @@ async def test_error_handling(api_client):
# Invalid request (missing required field)
response = await api_client.post(
"/api/v1/agents/error_test/memories",
"/v1/default/banks/error_test/memories",
json={
"items": [
{
@ -284,19 +332,19 @@ async def test_error_handling(api_client):
)
assert response.status_code == 422 # Validation error
# Search with invalid parameters
# Recall with invalid parameters
response = await api_client.post(
"/api/v1/agents/error_test/memories/search",
"/v1/default/banks/error_test/memories/recall",
json={
"query": "test",
"thinking_budget": -1 # Invalid negative budget
"budget": "invalid_budget" # Invalid budget value (should be low/mid/high)
}
)
assert response.status_code == 422
# Get non-existent document
response = await api_client.get(
"/api/v1/agents/nonexistent_agent/documents/fake-doc-id"
"/v1/default/banks/nonexistent_bank/documents/fake-doc-id"
)
assert response.status_code == 404
@ -306,7 +354,7 @@ async def test_error_handling(api_client):
@pytest.mark.asyncio
async def test_concurrent_requests(api_client):
"""Test that API can handle concurrent requests."""
agent_id = f"concurrent_test_{datetime.now().timestamp()}"
bank_id = f"concurrent_test_{datetime.now().timestamp()}"
# Store multiple memories concurrently (simulated with sequential calls)
responses = []
@ -319,7 +367,7 @@ async def test_concurrent_requests(api_client):
]
for fact in test_facts:
response = await api_client.post(
f"/api/v1/agents/{agent_id}/memories",
f"/v1/default/banks/{bank_id}/memories",
json={
"items": [
{
@ -337,7 +385,7 @@ async def test_concurrent_requests(api_client):
# Verify all facts stored
response = await api_client.get(
f"/api/v1/agents/{agent_id}/memories/list",
f"/v1/default/banks/{bank_id}/memories/list",
params={"limit": 20}
)
assert response.status_code == 200
@ -350,11 +398,11 @@ async def test_concurrent_requests(api_client):
@pytest.mark.asyncio
async def test_document_deletion(api_client):
"""Test document deletion including cascade deletion of memory units and links."""
test_agent_id = f"doc_delete_test_{datetime.now().timestamp()}"
test_bank_id = f"doc_delete_test_{datetime.now().timestamp()}"
# Store a document with memory
response = await api_client.post(
f"/api/v1/agents/{test_agent_id}/memories",
f"/v1/default/banks/{test_bank_id}/memories",
json={
"items": [
{
@ -370,7 +418,7 @@ async def test_document_deletion(api_client):
# Verify document exists
response = await api_client.get(
f"/api/v1/agents/{test_agent_id}/documents/sales-report-q1-2024"
f"/v1/default/banks/{test_bank_id}/documents/sales-report-q1-2024"
)
assert response.status_code == 200
doc_info = response.json()
@ -380,7 +428,7 @@ async def test_document_deletion(api_client):
# Delete the document
response = await api_client.delete(
f"/api/v1/agents/{test_agent_id}/documents/sales-report-q1-2024"
f"/v1/default/banks/{test_bank_id}/documents/sales-report-q1-2024"
)
assert response.status_code == 200
delete_result = response.json()
@ -391,13 +439,13 @@ async def test_document_deletion(api_client):
# Verify document is gone (should return 404)
response = await api_client.get(
f"/api/v1/agents/{test_agent_id}/documents/sales-report-q1-2024"
f"/v1/default/banks/{test_bank_id}/documents/sales-report-q1-2024"
)
assert response.status_code == 404
print("Document deletion verified - returns 404")
# Verify document is not in the list
response = await api_client.get(f"/api/v1/agents/{test_agent_id}/documents")
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/documents")
assert response.status_code == 200
documents = response.json()
doc_ids = [doc["id"] for doc in documents["items"]]
@ -406,7 +454,7 @@ async def test_document_deletion(api_client):
# Try to delete again (should return 404)
response = await api_client.delete(
f"/api/v1/agents/{test_agent_id}/documents/sales-report-q1-2024"
f"/v1/default/banks/{test_bank_id}/documents/sales-report-q1-2024"
)
assert response.status_code == 404
print("Double delete returns 404 - verified")

View file

@ -2,6 +2,7 @@
Test observation generation and entity state functionality.
"""
import pytest
from hindsight_api.engine.memory_engine import Budget
from datetime import datetime, timezone
@ -14,19 +15,19 @@ async def test_observation_generation_on_put(memory):
2. Wait for background tasks (observation generation)
3. Verify observations were created and linked to the entity
"""
agent_id = f"test_obs_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_obs_{datetime.now(timezone.utc).timestamp()}"
try:
# Store some facts about an entity
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="John is a software engineer at Google. He is detail-oriented and methodical.",
context="work info",
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc)
)
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="John has been working on the AI team for 3 years. He specializes in machine learning.",
context="work info",
event_date=datetime(2024, 2, 1, tzinfo=timezone.utc)
@ -42,10 +43,10 @@ async def test_observation_generation_on_put(memory):
"""
SELECT id, canonical_name
FROM entities
WHERE agent_id = $1 AND LOWER(canonical_name) LIKE '%john%'
WHERE bank_id = $1 AND LOWER(canonical_name) LIKE '%john%'
LIMIT 1
""",
agent_id
bank_id
)
if entity_row:
@ -55,7 +56,7 @@ async def test_observation_generation_on_put(memory):
print(f"Entity: {entity_name} (id: {entity_id})")
# Get observations for the entity
observations = await memory.get_entity_observations(agent_id, entity_id, limit=10)
observations = await memory.get_entity_observations(bank_id, entity_id, limit=10)
print(f"\n=== Observations for {entity_name} ===")
print(f"Total observations: {len(observations)}")
@ -79,8 +80,8 @@ async def test_observation_generation_on_put(memory):
# Cleanup
pool = await memory._get_pool()
async with pool.acquire() as conn:
await conn.execute("DELETE FROM memory_units WHERE agent_id = $1", agent_id)
await conn.execute("DELETE FROM entities WHERE agent_id = $1", agent_id)
await conn.execute("DELETE FROM memory_units WHERE bank_id = $1", bank_id)
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
@pytest.mark.asyncio
@ -88,12 +89,12 @@ async def test_regenerate_entity_observations(memory):
"""
Test explicit regeneration of observations for an entity.
"""
agent_id = f"test_regen_obs_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_regen_obs_{datetime.now(timezone.utc).timestamp()}"
try:
# Store facts about an entity
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Sarah is a product manager who loves user research and data analysis.",
context="work info",
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc)
@ -108,10 +109,10 @@ async def test_regenerate_entity_observations(memory):
"""
SELECT id, canonical_name
FROM entities
WHERE agent_id = $1 AND LOWER(canonical_name) LIKE '%sarah%'
WHERE bank_id = $1 AND LOWER(canonical_name) LIKE '%sarah%'
LIMIT 1
""",
agent_id
bank_id
)
if entity_row:
@ -120,7 +121,7 @@ async def test_regenerate_entity_observations(memory):
# Manually regenerate observations
created_ids = await memory.regenerate_entity_observations(
agent_id=agent_id,
bank_id=bank_id,
entity_id=entity_id,
entity_name=entity_name
)
@ -129,7 +130,7 @@ async def test_regenerate_entity_observations(memory):
print(f"Created {len(created_ids)} observations for {entity_name}")
# Get the observations
observations = await memory.get_entity_observations(agent_id, entity_id, limit=10)
observations = await memory.get_entity_observations(bank_id, entity_id, limit=10)
for obs in observations:
print(f" - {obs.text}")
@ -147,8 +148,8 @@ async def test_regenerate_entity_observations(memory):
# Cleanup
pool = await memory._get_pool()
async with pool.acquire() as conn:
await conn.execute("DELETE FROM memory_units WHERE agent_id = $1", agent_id)
await conn.execute("DELETE FROM entities WHERE agent_id = $1", agent_id)
await conn.execute("DELETE FROM memory_units WHERE bank_id = $1", bank_id)
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
@pytest.mark.asyncio
@ -156,19 +157,19 @@ async def test_search_with_include_entities(memory):
"""
Test that search with include_entities=True returns entity observations.
"""
agent_id = f"test_search_ent_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_search_ent_{datetime.now(timezone.utc).timestamp()}"
try:
# Store facts about entities
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Alice is a data scientist who works on recommendation systems at Netflix.",
context="work info",
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc)
)
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Alice presented her research at the ML conference last month. She is an expert in deep learning.",
context="work info",
event_date=datetime(2024, 2, 1, tzinfo=timezone.utc)
@ -178,11 +179,11 @@ async def test_search_with_include_entities(memory):
await memory.wait_for_background_tasks()
# Search with include_entities=True
result = await memory.search_async(
agent_id=agent_id,
result = await memory.recall_async(
bank_id=bank_id,
query="What does Alice do?",
fact_type=["world", "agent"],
thinking_budget=30,
budget=Budget.LOW, # 30,
max_tokens=2000,
include_entities=True,
max_entity_tokens=500
@ -223,8 +224,8 @@ async def test_search_with_include_entities(memory):
# Cleanup
pool = await memory._get_pool()
async with pool.acquire() as conn:
await conn.execute("DELETE FROM memory_units WHERE agent_id = $1", agent_id)
await conn.execute("DELETE FROM entities WHERE agent_id = $1", agent_id)
await conn.execute("DELETE FROM memory_units WHERE bank_id = $1", bank_id)
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
@pytest.mark.asyncio
@ -232,12 +233,12 @@ async def test_get_entity_state(memory):
"""
Test getting the full state of an entity.
"""
agent_id = f"test_entity_state_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_entity_state_{datetime.now(timezone.utc).timestamp()}"
try:
# Store facts
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Bob is a frontend developer who specializes in React and TypeScript.",
context="work info",
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc)
@ -252,10 +253,10 @@ async def test_get_entity_state(memory):
"""
SELECT id, canonical_name
FROM entities
WHERE agent_id = $1 AND LOWER(canonical_name) LIKE '%bob%'
WHERE bank_id = $1 AND LOWER(canonical_name) LIKE '%bob%'
LIMIT 1
""",
agent_id
bank_id
)
if entity_row:
@ -264,7 +265,7 @@ async def test_get_entity_state(memory):
# Get entity state
state = await memory.get_entity_state(
agent_id=agent_id,
bank_id=bank_id,
entity_id=entity_id,
entity_name=entity_name,
limit=10
@ -284,8 +285,8 @@ async def test_get_entity_state(memory):
# Cleanup
pool = await memory._get_pool()
async with pool.acquire() as conn:
await conn.execute("DELETE FROM memory_units WHERE agent_id = $1", agent_id)
await conn.execute("DELETE FROM entities WHERE agent_id = $1", agent_id)
await conn.execute("DELETE FROM memory_units WHERE bank_id = $1", bank_id)
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
@pytest.mark.asyncio
@ -293,12 +294,12 @@ async def test_observation_fact_type_in_database(memory):
"""
Test that observations are stored with correct fact_type in database.
"""
agent_id = f"test_obs_db_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_obs_db_{datetime.now(timezone.utc).timestamp()}"
try:
# Store facts
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Charlie is a DevOps engineer who manages the Kubernetes infrastructure.",
context="work info",
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc)
@ -313,9 +314,9 @@ async def test_observation_fact_type_in_database(memory):
"""
SELECT id, text, fact_type, context
FROM memory_units
WHERE agent_id = $1 AND fact_type = 'observation'
WHERE bank_id = $1 AND fact_type = 'observation'
""",
agent_id
bank_id
)
print(f"\n=== Observation Records in Database ===")
@ -334,5 +335,5 @@ async def test_observation_fact_type_in_database(memory):
# Cleanup
pool = await memory._get_pool()
async with pool.acquire() as conn:
await conn.execute("DELETE FROM memory_units WHERE agent_id = $1", agent_id)
await conn.execute("DELETE FROM entities WHERE agent_id = $1", agent_id)
await conn.execute("DELETE FROM memory_units WHERE bank_id = $1", bank_id)
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)

View file

@ -2,6 +2,7 @@
Test search tracing functionality.
"""
import pytest
from hindsight_api.engine.memory_engine import Budget
from hindsight_api import SearchTrace
from datetime import datetime, timezone
@ -10,33 +11,33 @@ from datetime import datetime, timezone
async def test_search_with_trace(memory):
"""Test that search with enable_trace=True returns a valid SearchTrace."""
# Generate a unique agent ID for this test
agent_id = f"test_trace_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_trace_{datetime.now(timezone.utc).timestamp()}"
try:
# Store some test memories
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Alice works at Google in Mountain View",
context="test context",
)
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Bob also works at Google but in New York",
context="test context",
)
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Charlie founded a startup called TechCorp",
context="test context",
)
# Search with tracing enabled
search_result = await memory.search_async(
agent_id=agent_id,
search_result = await memory.recall_async(
bank_id=bank_id,
query="Who works at Google?",
fact_type=["world"],
thinking_budget=20,
budget=Budget.LOW, # 20,
max_tokens=512,
enable_trace=True,
)
@ -51,7 +52,7 @@ async def test_search_with_trace(memory):
# Verify query info
assert trace["query"]["query_text"] == "Who works at Google?"
assert trace["query"]["thinking_budget"] == 20
assert trace["query"]["budget"] == 100 # Budget.LOW = 100
assert trace["query"]["max_tokens"] == 512
assert len(trace["query"]["query_embedding"]) > 0, "Query embedding should be populated"
@ -80,7 +81,7 @@ async def test_search_with_trace(memory):
# Verify summary
assert trace["summary"]["total_nodes_visited"] == len(trace["visits"])
assert trace["summary"]["results_returned"] == len(search_result.results)
assert trace["summary"]["budget_used"] <= trace["query"]["thinking_budget"]
assert trace["summary"]["budget_used"] <= trace["query"]["budget"]
assert trace["summary"]["total_duration_seconds"] > 0
# Verify phase metrics
@ -101,29 +102,29 @@ async def test_search_with_trace(memory):
finally:
# Cleanup
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)
@pytest.mark.asyncio
async def test_search_without_trace(memory):
"""Test that search with enable_trace=False returns None for trace."""
agent_id = f"test_no_trace_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_no_trace_{datetime.now(timezone.utc).timestamp()}"
try:
# Store a test memory
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Test memory without trace",
context="test",
)
# Search without tracing
search_result = await memory.search_async(
agent_id=agent_id,
search_result = await memory.recall_async(
bank_id=bank_id,
query="test",
fact_type=["world"],
thinking_budget=10,
budget=Budget.LOW, # 10,
max_tokens=512,
enable_trace=False,
)
@ -136,4 +137,4 @@ async def test_search_without_trace(memory):
finally:
# Cleanup
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)

View file

@ -4,6 +4,7 @@ import os
from datetime import datetime, timezone, timedelta
import pytest
from hindsight_api import MemoryEngine
from hindsight_api.engine.memory_engine import Budget
@pytest.mark.asyncio
@ -19,11 +20,11 @@ async def test_temporal_ranges_are_written():
)
await memory.initialize()
agent_id = "test_temporal_ranges"
bank_id = "test_temporal_ranges"
# Clean up any existing data
try:
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)
except Exception:
pass
@ -31,8 +32,8 @@ async def test_temporal_ranges_are_written():
conversation_date = datetime(2024, 11, 17, 10, 0, 0, tzinfo=timezone.utc)
text1 = "Yesterday I went to a pottery workshop where I made a beautiful vase."
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content=text1,
event_date=conversation_date
)
@ -40,8 +41,8 @@ async def test_temporal_ranges_are_written():
# Test 2: Period event (month range)
text2 = "In February 2024, Alice visited Paris and explored the Louvre museum."
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content=text2,
event_date=conversation_date
)
@ -56,10 +57,10 @@ async def test_temporal_ranges_are_written():
"""
SELECT id, text, event_date, occurred_start, occurred_end, mentioned_at
FROM memory_units
WHERE agent_id = $1
WHERE bank_id = $1
ORDER BY created_at
""",
agent_id
bank_id
)
print(f"\n\n=== Retrieved {len(rows)} facts ===")
@ -113,11 +114,11 @@ async def test_temporal_ranges_are_written():
# Test search results also include temporal fields
print("\n=== Testing Search Results ===")
search_result = await memory.search_async(
agent_id=agent_id,
search_result = await memory.recall_async(
bank_id=bank_id,
query="pottery workshop",
fact_type=["event", "world"],
thinking_budget=20,
budget=Budget.LOW,
max_tokens=4096
)
@ -136,7 +137,7 @@ async def test_temporal_ranges_are_written():
print("⚠ Temporal fields not yet populated in search results (known issue)")
# Clean up
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)
await memory.close()

View file

@ -3,6 +3,7 @@ Test think function for opinion generation and consistency.
"""
import pytest
from datetime import datetime, timezone
from hindsight_api.engine.memory_engine import Budget
@pytest.mark.asyncio
@ -13,20 +14,20 @@ async def test_think_opinion_consistency(memory):
2. Stores the opinion in the database
3. Returns consistent response on subsequent calls with the same query
"""
agent_id = f"test_think_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_think_{datetime.now(timezone.utc).timestamp()}"
try:
# Store some initial facts to give context for opinion formation
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Alice is a software engineer who has worked on 5 major projects. She always delivers on time and writes clean, well-documented code.",
context="performance review",
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc)
)
await memory.put_async(
agent_id=agent_id,
await memory.retain_async(
bank_id=bank_id,
content="Bob recently joined the team. He missed his first deadline and his code had many bugs.",
context="performance review",
event_date=datetime(2024, 2, 1, tzinfo=timezone.utc)
@ -34,25 +35,20 @@ async def test_think_opinion_consistency(memory):
# First think call - should generate opinions
query = "Who is a more reliable engineer?"
result1 = await memory.think_async(
agent_id=agent_id,
result1 = await memory.reflect_async(
bank_id=bank_id,
query=query,
thinking_budget=30,
budget=Budget.LOW,
)
print(f"\n=== First Think Call ===")
print(f"Answer: {result1.text}")
print(f"New opinions formed: {len(result1.new_opinions)}")
# Verify we got an answer
assert result1.text, "First think call should return an answer"
assert result1.based_on, "Should return based_on facts"
# Verify opinions were formed
new_opinions_count = len(result1.new_opinions)
print(f"\nNew opinions formed: {new_opinions_count}")
# Wait for background opinion PUT tasks to complete
# Wait for background opinion processing tasks to complete
await memory.wait_for_background_tasks()
# Search for stored opinions to verify they were actually saved
@ -62,10 +58,10 @@ async def test_think_opinion_consistency(memory):
"""
SELECT id, text, confidence_score, fact_type
FROM memory_units
WHERE agent_id = $1 AND fact_type = 'opinion'
WHERE bank_id = $1 AND fact_type = 'opinion'
ORDER BY created_at DESC
""",
agent_id
bank_id
)
print(f"\n=== Stored Opinions in Database ===")
@ -82,10 +78,10 @@ async def test_think_opinion_consistency(memory):
print(f"⚠ Note: No opinions were extracted/stored (this can happen if the LLM response format doesn't trigger opinion extraction)")
# Second think call - should use the stored opinions
result2 = await memory.think_async(
agent_id=agent_id,
result2 = await memory.reflect_async(
bank_id=bank_id,
query=query,
thinking_budget=30,
budget=Budget.LOW,
)
print(f"\n=== Second Think Call ===")
@ -93,7 +89,6 @@ async def test_think_opinion_consistency(memory):
print(f"Existing opinions used: {len(result2.based_on.get('opinion', []))}")
for opinion in result2.based_on.get('opinion', []):
print(f" - {opinion.text}")
print(f"New opinions formed: {len(result2.new_opinions)}")
# Verify second call also got an answer
assert result2.text, "Second think call should return an answer"
@ -127,7 +122,7 @@ async def test_think_opinion_consistency(memory):
finally:
# Clean up agent data
try:
await memory.delete_agent(agent_id)
await memory.delete_bank(bank_id)
except Exception as e:
print(f"Warning: Error during cleanup: {e}")
@ -137,13 +132,13 @@ async def test_think_without_prior_context(memory):
"""
Test that think function handles queries when there's no relevant context.
"""
agent_id = f"test_think_no_context_{datetime.now(timezone.utc).timestamp()}"
bank_id = f"test_think_no_context_{datetime.now(timezone.utc).timestamp()}"
# Call think without storing any prior facts
result = await memory.think_async(
agent_id=agent_id,
result = await memory.reflect_async(
bank_id=bank_id,
query="What is the capital of France?",
thinking_budget=20,
budget=Budget.LOW,
)
print(f"\n=== Think Without Context ===")

View file

@ -11,14 +11,16 @@ name = "hindsight"
path = "src/main.rs"
[dependencies]
# Hindsight API client (generated)
hindsight-client = { path = "../hindsight-clients/rust" }
# CLI framework
clap = { version = "4.5", features = ["derive", "env"] }
# HTTP client
reqwest = { version = "0.12", features = ["json", "blocking"] }
# Async runtime
tokio = { version = "1", features = ["full"] }
# Serialization
# Serialization (for config and output formatting)
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"

View file

@ -1,134 +1,16 @@
use anyhow::{Context, Result};
use reqwest::blocking::Client;
//! API client wrapper
//!
//! This module provides a thin wrapper around the auto-generated hindsight-client
//! to bridge from the CLI's synchronous code to the async API client.
use anyhow::Result;
use hindsight_client::Client as AsyncClient;
pub use hindsight_client::types;
use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Serialize)]
pub struct SearchRequest {
pub query: String,
pub fact_type: Vec<String>,
pub thinking_budget: i32,
pub max_tokens: i32,
pub trace: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchResponse {
pub results: Vec<Fact>,
pub trace: Option<TraceInfo>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Fact {
#[serde(default)]
pub id: Option<String>,
pub text: String,
#[serde(rename = "type", default)]
pub fact_type: Option<String>,
pub activation: Option<f64>,
#[serde(default)]
pub context: Option<String>,
#[serde(default)]
pub event_date: Option<String>,
#[serde(default)]
pub occurred_start: Option<String>,
#[serde(default)]
pub occurred_end: Option<String>,
#[serde(default)]
pub mentioned_at: Option<String>,
#[serde(default)]
pub document_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TraceInfo {
pub total_time: Option<f64>,
pub activation_count: Option<i32>,
}
#[derive(Debug, Serialize)]
pub struct ThinkRequest {
pub query: String,
pub thinking_budget: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ThinkResponse {
pub text: String,
pub based_on: Vec<Fact>,
pub new_opinions: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct MemoryItem {
pub content: String,
pub context: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct BatchMemoryRequest {
pub items: Vec<MemoryItem>,
pub document_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BatchMemoryResponse {
pub success: bool,
pub stored_count: Option<i32>,
pub items_count: Option<i32>,
pub error: Option<String>,
pub job_id: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum AgentsResponse {
Success {
agents: Vec<AgentProfile>,
},
Error {
error: String,
},
}
#[derive(Debug, Serialize)]
pub struct Agent {
pub agent_id: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct PersonalityTraits {
pub openness: f32,
pub conscientiousness: f32,
pub extraversion: f32,
pub agreeableness: f32,
pub neuroticism: f32,
pub bias_strength: f32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AgentProfile {
pub agent_id: String,
pub name: String,
pub personality: PersonalityTraits,
pub background: String,
}
#[derive(Debug, Serialize)]
pub struct AddBackgroundRequest {
pub content: String,
pub update_personality: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BackgroundResponse {
pub background: String,
pub personality: Option<PersonalityTraits>,
}
// Types not defined in OpenAPI spec (TODO: add to openapi.json)
#[derive(Debug, Serialize, Deserialize)]
pub struct AgentStats {
pub agent_id: String,
@ -143,36 +25,6 @@ pub struct AgentStats {
pub failed_operations: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Document {
pub id: String,
pub agent_id: String,
pub content_hash: Option<String>,
pub created_at: String,
pub updated_at: String,
pub text_length: i32,
pub memory_unit_count: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DocumentDetails {
pub id: String,
pub agent_id: String,
pub original_text: String,
pub content_hash: Option<String>,
pub created_at: String,
pub updated_at: String,
pub memory_unit_count: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DocumentsResponse {
pub items: Vec<Document>,
pub total: i32,
pub limit: i32,
pub offset: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Operation {
pub id: String,
@ -191,628 +43,210 @@ pub struct OperationsResponse {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteResponse {
pub success: bool,
pub message: String,
pub struct TraceInfo {
pub total_time: Option<f64>,
pub activation_count: Option<i32>,
}
// Unified result for put_memories that handles both sync and async responses
#[derive(Debug, Serialize, Deserialize)]
pub struct MemoryPutResult {
pub success: bool,
pub items_count: i64,
pub message: String,
pub is_async: bool,
}
#[derive(Clone)]
pub struct ApiClient {
client: Client,
base_url: String,
client: AsyncClient,
runtime: std::sync::Arc<tokio::runtime::Runtime>,
}
impl ApiClient {
pub fn new(base_url: String) -> Result<Self> {
let client = Client::builder()
.timeout(Duration::from_secs(60))
.build()
.context("Failed to create HTTP client")?;
Ok(ApiClient { client, base_url })
let runtime = std::sync::Arc::new(tokio::runtime::Runtime::new()?);
let client = AsyncClient::new(&base_url);
Ok(ApiClient { client, runtime })
}
pub fn search(&self, agent_id: &str, request: SearchRequest, verbose: bool) -> Result<SearchResponse> {
let url = format!("{}/api/v1/agents/{}/memories/search", self.base_url, agent_id);
let request_body = serde_json::to_string_pretty(&request).unwrap_or_default();
if verbose {
eprintln!("Request URL: {}", url);
eprintln!("Request body:\n{}", request_body);
pub fn list_agents(&self, _verbose: bool) -> Result<Vec<types::BankListItem>> {
self.runtime.block_on(async {
let response = self.client.list_banks().await?;
Ok(response.into_inner().banks)
})
}
let response = self
.client
.post(&url)
.json(&request)
.timeout(Duration::from_secs(120))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
pub fn get_profile(&self, agent_id: &str, _verbose: bool) -> Result<types::BankProfileResponse> {
self.runtime.block_on(async {
let response = self.client.get_bank_profile(agent_id).await?;
Ok(response.into_inner())
})
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
pub fn get_stats(&self, agent_id: &str, _verbose: bool) -> Result<AgentStats> {
self.runtime.block_on(async {
let response = self.client.get_agent_stats(agent_id).await?;
let value = response.into_inner();
let stats: AgentStats = serde_json::from_value(value)?;
Ok(stats)
})
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: SearchResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn think(&self, agent_id: &str, request: ThinkRequest, verbose: bool) -> Result<ThinkResponse> {
let url = format!("{}/api/v1/agents/{}/think", self.base_url, agent_id);
if verbose {
eprintln!("Request URL: {}", url);
eprintln!("Request body:\n{}", serde_json::to_string_pretty(&request).unwrap_or_default());
}
let response = self
.client
.post(&url)
.json(&request)
.timeout(Duration::from_secs(120))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: ThinkResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn put_memories(&self, agent_id: &str, request: BatchMemoryRequest, async_mode: bool, verbose: bool) -> Result<BatchMemoryResponse> {
let endpoint = if async_mode {
"async"
} else {
""
pub fn update_agent_name(&self, agent_id: &str, name: &str, _verbose: bool) -> Result<types::BankProfileResponse> {
self.runtime.block_on(async {
let request = types::CreateBankRequest {
name: Some(name.to_string()),
background: None,
personality: None,
};
let url = if async_mode {
format!("{}/api/v1/agents/{}/memories/{}", self.base_url, agent_id, endpoint)
} else {
format!("{}/api/v1/agents/{}/memories", self.base_url, agent_id)
};
if verbose {
eprintln!("Request URL: {}", url);
eprintln!("Request body:\n{}", serde_json::to_string_pretty(&request).unwrap_or_default());
let response = self.client.create_or_update_bank(agent_id, &request).await?;
Ok(response.into_inner())
})
}
let response = self
.client
.post(&url)
.json(&request)
.timeout(Duration::from_secs(120))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: BatchMemoryResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn list_agents(&self, verbose: bool) -> Result<Vec<Agent>> {
let url = format!("{}/api/v1/agents", self.base_url);
if verbose {
eprintln!("Request URL: {}", url);
}
let response = self
.client
.get(&url)
.timeout(Duration::from_secs(30))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: AgentsResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
match result {
AgentsResponse::Success { agents } => {
Ok(agents.into_iter().map(|profile| Agent { agent_id: profile.agent_id }).collect())
}
AgentsResponse::Error { error } => {
anyhow::bail!("Failed to list agents: {}", error)
}
}
}
pub fn get_profile(&self, agent_id: &str, verbose: bool) -> Result<AgentProfile> {
let url = format!("{}/api/v1/agents/{}/profile", self.base_url, agent_id);
if verbose {
eprintln!("Request URL: {}", url);
}
let response = self
.client
.get(&url)
.timeout(Duration::from_secs(30))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: AgentProfile = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn update_agent_name(
&self,
agent_id: &str,
name: &str,
verbose: bool,
) -> Result<AgentProfile> {
#[derive(Serialize)]
struct UpdateNameRequest {
name: String,
}
let url = format!("{}/api/v1/agents/{}", self.base_url, agent_id);
let request = UpdateNameRequest {
name: name.to_string(),
};
if verbose {
eprintln!("Request URL: {}", url);
eprintln!("Request body:\n{}", serde_json::to_string_pretty(&request).unwrap_or_default());
}
let response = self
.client
.put(&url)
.json(&request)
.timeout(Duration::from_secs(30))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: AgentProfile = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn add_background(&self, agent_id: &str, content: &str, update_personality: bool, verbose: bool) -> Result<BackgroundResponse> {
let url = format!("{}/api/v1/agents/{}/background", self.base_url, agent_id);
let request = AddBackgroundRequest {
pub fn add_background(&self, agent_id: &str, content: &str, update_personality: bool, _verbose: bool) -> Result<types::BackgroundResponse> {
self.runtime.block_on(async {
let request = types::AddBackgroundRequest {
content: content.to_string(),
update_personality,
};
if verbose {
eprintln!("Request URL: {}", url);
eprintln!("Request body:\n{}", serde_json::to_string_pretty(&request).unwrap_or_default());
let response = self.client.add_bank_background(agent_id, &request).await?;
Ok(response.into_inner())
})
}
let response = self
.client
.post(&url)
.json(&request)
.timeout(Duration::from_secs(60))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
pub fn recall(&self, agent_id: &str, request: &types::RecallRequest, _verbose: bool) -> Result<types::RecallResponse> {
self.runtime.block_on(async {
let response = self.client.recall_memories(agent_id, request).await?;
Ok(response.into_inner())
})
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
pub fn reflect(&self, agent_id: &str, request: &types::ReflectRequest, _verbose: bool) -> Result<types::ReflectResponse> {
self.runtime.block_on(async {
let response = self.client.reflect(agent_id, request).await?;
Ok(response.into_inner())
})
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
pub fn retain(&self, agent_id: &str, request: &types::RetainRequest, _async_mode: bool, _verbose: bool) -> Result<MemoryPutResult> {
self.runtime.block_on(async {
let response = self.client.retain_memories(agent_id, request).await?;
let result = response.into_inner();
Ok(MemoryPutResult {
success: result.success,
items_count: result.items_count,
message: format!("Stored {} memory units", result.items_count),
is_async: result.async_,
})
})
}
let result: BackgroundResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
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.")
}
pub fn clear_memories(&self, agent_id: &str, fact_type: Option<&str>, _verbose: bool) -> Result<types::DeleteResponse> {
self.runtime.block_on(async {
let response = self.client.clear_bank_memories(agent_id, fact_type).await?;
Ok(response.into_inner())
})
}
pub fn list_documents(&self, agent_id: &str, q: Option<&str>, limit: Option<i32>, offset: Option<i32>, _verbose: bool) -> Result<types::ListDocumentsResponse> {
self.runtime.block_on(async {
let response = self.client.list_documents(
agent_id,
limit.map(|l| l as i64),
offset.map(|o| o as i64),
q
).await?;
Ok(response.into_inner())
})
}
pub fn get_document(&self, agent_id: &str, document_id: &str, _verbose: bool) -> Result<types::DocumentResponse> {
self.runtime.block_on(async {
let response = self.client.get_document(agent_id, document_id).await?;
Ok(response.into_inner())
})
}
pub fn delete_document(&self, agent_id: &str, document_id: &str, _verbose: bool) -> Result<types::DeleteResponse> {
self.runtime.block_on(async {
let response = self.client.delete_document(agent_id, document_id).await?;
let value = response.into_inner();
let result: types::DeleteResponse = serde_json::from_value(value)?;
Ok(result)
})
}
pub fn get_stats(&self, agent_id: &str, verbose: bool) -> Result<AgentStats> {
let url = format!("{}/api/v1/agents/{}/stats", self.base_url, agent_id);
if verbose {
eprintln!("Request URL: {}", url);
pub fn list_operations(&self, agent_id: &str, _verbose: bool) -> Result<OperationsResponse> {
self.runtime.block_on(async {
let response = self.client.list_operations(agent_id).await?;
let value = response.into_inner();
let ops: OperationsResponse = serde_json::from_value(value)?;
Ok(ops)
})
}
let response = self
.client
.get(&url)
.timeout(Duration::from_secs(30))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: AgentStats = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
pub fn cancel_operation(&self, agent_id: &str, operation_id: &str, _verbose: bool) -> Result<types::DeleteResponse> {
self.runtime.block_on(async {
let response = self.client.cancel_operation(agent_id, operation_id).await?;
let value = response.into_inner();
let result: types::DeleteResponse = serde_json::from_value(value)?;
Ok(result)
})
}
pub fn list_documents(&self, agent_id: &str, q: Option<&str>, limit: Option<i32>, offset: Option<i32>, verbose: bool) -> Result<DocumentsResponse> {
let mut url = format!("{}/api/v1/agents/{}/documents", self.base_url, agent_id);
let mut params = vec![];
if let Some(query) = q {
params.push(format!("q={}", query));
}
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
if let Some(o) = offset {
params.push(format!("offset={}", o));
pub fn list_memories(&self, bank_id: &str, type_filter: Option<&str>, q: Option<&str>, limit: Option<i64>, offset: Option<i64>, _verbose: bool) -> Result<types::ListMemoryUnitsResponse> {
self.runtime.block_on(async {
let response = self.client.list_memories(bank_id, limit, offset, q, type_filter).await?;
Ok(response.into_inner())
})
}
if !params.is_empty() {
url.push('?');
url.push_str(&params.join("&"));
pub fn list_entities(&self, bank_id: &str, limit: Option<i64>, _verbose: bool) -> Result<types::EntityListResponse> {
self.runtime.block_on(async {
let response = self.client.list_entities(bank_id, limit).await?;
Ok(response.into_inner())
})
}
if verbose {
eprintln!("Request URL: {}", url);
pub fn get_entity(&self, bank_id: &str, entity_id: &str, _verbose: bool) -> Result<types::EntityDetailResponse> {
self.runtime.block_on(async {
let response = self.client.get_entity(bank_id, entity_id).await?;
Ok(response.into_inner())
})
}
let response = self
.client
.get(&url)
.timeout(Duration::from_secs(30))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: DocumentsResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn get_document(&self, agent_id: &str, document_id: &str, verbose: bool) -> Result<DocumentDetails> {
let url = format!("{}/api/v1/agents/{}/documents/{}", self.base_url, agent_id, document_id);
if verbose {
eprintln!("Request URL: {}", url);
}
let response = self
.client
.get(&url)
.timeout(Duration::from_secs(30))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: DocumentDetails = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn list_operations(&self, agent_id: &str, verbose: bool) -> Result<OperationsResponse> {
let url = format!("{}/api/v1/agents/{}/operations", self.base_url, agent_id);
if verbose {
eprintln!("Request URL: {}", url);
}
let response = self
.client
.get(&url)
.timeout(Duration::from_secs(30))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: OperationsResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn cancel_operation(&self, agent_id: &str, operation_id: &str, verbose: bool) -> Result<DeleteResponse> {
let url = format!("{}/api/v1/agents/{}/operations/{}", self.base_url, agent_id, operation_id);
if verbose {
eprintln!("Request URL: {}", url);
}
let response = self
.client
.delete(&url)
.timeout(Duration::from_secs(30))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: DeleteResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn delete_memory(&self, agent_id: &str, unit_id: &str, verbose: bool) -> Result<DeleteResponse> {
let url = format!("{}/api/v1/agents/{}/memories/{}", self.base_url, agent_id, unit_id);
if verbose {
eprintln!("Request URL: {}", url);
}
let response = self
.client
.delete(&url)
.timeout(Duration::from_secs(30))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: DeleteResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn delete_document(&self, agent_id: &str, document_id: &str, verbose: bool) -> Result<DeleteResponse> {
let url = format!("{}/api/v1/agents/{}/documents/{}", self.base_url, agent_id, document_id);
if verbose {
eprintln!("Request URL: {}", url);
}
let response = self
.client
.delete(&url)
.timeout(Duration::from_secs(30))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: DeleteResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
}
pub fn clear_memories(&self, agent_id: &str, fact_type: Option<&str>, verbose: bool) -> Result<DeleteResponse> {
let mut url = format!("{}/api/v1/agents/{}/memories", self.base_url, agent_id);
if let Some(ft) = fact_type {
url.push_str(&format!("?fact_type={}", ft));
}
if verbose {
eprintln!("Request URL: {}", url);
}
let response = self
.client
.delete(&url)
.timeout(Duration::from_secs(60))
.send()?;
let status = response.status();
if verbose {
eprintln!("Response status: {}", status);
}
if !status.is_success() {
let error_body = response.text().unwrap_or_default();
if verbose {
eprintln!("Error response body:\n{}", error_body);
}
anyhow::bail!("API returned error status {}: {}", status, error_body);
}
let response_text = response.text()?;
if verbose {
eprintln!("Response body:\n{}", response_text);
}
let result: DeleteResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse API response. Response was: {}", response_text))?;
Ok(result)
pub fn regenerate_entity(&self, bank_id: &str, entity_id: &str, _verbose: bool) -> Result<types::EntityDetailResponse> {
self.runtime.block_on(async {
let response = self.client.regenerate_entity_observations(bank_id, entity_id).await?;
Ok(response.into_inner())
})
}
}
// Re-export types from the generated client for use in commands
pub use types::{
AddBackgroundRequest,
BackgroundResponse,
BankListItem,
BankProfileResponse,
CreateBankRequest,
DeleteResponse,
DocumentResponse,
ListDocumentsResponse,
MemoryItem,
PersonalityTraits,
RecallRequest,
RecallResponse,
RecallResult,
ReflectRequest,
ReflectResponse,
RetainRequest,
RetainResponse,
};

View file

@ -0,0 +1,248 @@
use anyhow::Result;
use crate::api::ApiClient;
use crate::output::{self, OutputFormat};
use crate::ui;
pub fn list(client: &ApiClient, verbose: bool, output_format: OutputFormat) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching banks..."))
} else {
None
};
let response = client.list_agents(verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(banks_list) => {
if output_format == OutputFormat::Pretty {
if banks_list.is_empty() {
ui::print_warning("No banks found");
} else {
ui::print_info(&format!("Found {} bank(s)", banks_list.len()));
for bank in &banks_list {
println!(" - {}", bank.bank_id);
}
}
} else {
output::print_output(&banks_list, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn profile(client: &ApiClient, bank_id: &str, verbose: bool, output_format: OutputFormat) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching profile..."))
} else {
None
};
let response = client.get_profile(bank_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(profile) => {
if output_format == OutputFormat::Pretty {
ui::print_profile(&profile);
} else {
output::print_output(&profile, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn stats(client: &ApiClient, bank_id: &str, verbose: bool, output_format: OutputFormat) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching statistics..."))
} else {
None
};
let response = client.get_stats(bank_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(stats) => {
if output_format == OutputFormat::Pretty {
ui::print_info(&format!("Statistics for bank '{}'", bank_id));
println!();
println!(" 📊 Overview");
println!(" Total Memory Units: {}", stats.total_nodes);
println!(" Total Links: {}", stats.total_links);
println!(" Total Documents: {}", stats.total_documents);
println!();
println!(" 🧠 Memory Units by Type");
let mut fact_types: Vec<_> = stats.nodes_by_fact_type.iter().collect();
fact_types.sort_by_key(|(k, _)| *k);
for (fact_type, count) in fact_types {
let icon = match fact_type.as_str() {
"world" => "🌍",
"agent" => "🤖",
"opinion" => "💭",
_ => ""
};
println!(" {} {:<10} {}", icon, fact_type, count);
}
println!();
println!(" 🔗 Links by Type");
let mut link_types: Vec<_> = stats.links_by_link_type.iter().collect();
link_types.sort_by_key(|(k, _)| *k);
for (link_type, count) in link_types {
let icon = match link_type.as_str() {
"temporal" => "",
"semantic" => "🔤",
"entity" => "🏷️",
_ => ""
};
println!(" {} {:<10} {}", icon, link_type, count);
}
println!();
println!(" 🔗 Links by Fact Type");
let mut fact_type_links: Vec<_> = stats.links_by_fact_type.iter().collect();
fact_type_links.sort_by_key(|(k, _)| *k);
for (fact_type, count) in fact_type_links {
let icon = match fact_type.as_str() {
"world" => "🌍",
"agent" => "🤖",
"opinion" => "💭",
_ => ""
};
println!(" {} {:<10} {}", icon, fact_type, count);
}
println!();
if !stats.links_breakdown.is_empty() {
println!(" 📈 Detailed Link Breakdown");
let mut fact_types: Vec<_> = stats.links_breakdown.iter().collect();
fact_types.sort_by_key(|(k, _)| *k);
for (fact_type, link_types) in fact_types {
let icon = match fact_type.as_str() {
"world" => "🌍",
"agent" => "🤖",
"opinion" => "💭",
_ => ""
};
println!(" {} {}", icon, fact_type);
let mut sorted_links: Vec<_> = link_types.iter().collect();
sorted_links.sort_by_key(|(k, _)| *k);
for (link_type, count) in sorted_links {
println!(" - {:<10} {}", link_type, count);
}
}
println!();
}
if stats.pending_operations > 0 || stats.failed_operations > 0 {
println!(" ⚙️ Operations");
if stats.pending_operations > 0 {
println!(" ⏳ Pending: {}", stats.pending_operations);
}
if stats.failed_operations > 0 {
println!(" ❌ Failed: {}", stats.failed_operations);
}
}
} else {
output::print_output(&stats, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn update_name(client: &ApiClient, bank_id: &str, name: &str, verbose: bool, output_format: OutputFormat) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Updating bank name..."))
} else {
None
};
let response = client.update_agent_name(bank_id, name, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(profile) => {
if output_format == OutputFormat::Pretty {
ui::print_success(&format!("Bank name updated to '{}'", profile.name));
} else {
output::print_output(&profile, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn update_background(
client: &ApiClient,
bank_id: &str,
content: &str,
no_update_personality: bool,
verbose: bool,
output_format: OutputFormat
) -> Result<()> {
let current_profile = if !no_update_personality {
client.get_profile(bank_id, verbose).ok()
} else {
None
};
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Merging background..."))
} else {
None
};
let response = client.add_background(bank_id, content, !no_update_personality, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(profile) => {
if output_format == OutputFormat::Pretty {
ui::print_success("Background updated successfully");
println!("\n{}", profile.background);
if !no_update_personality {
if let (Some(old_p), Some(new_p)) =
(current_profile.as_ref().map(|p| p.personality.clone()), &profile.personality)
{
println!("\nPersonality changes:");
println!(" Openness: {:.2}{:.2}", old_p.openness, new_p.openness);
println!(" Conscientiousness: {:.2}{:.2}", old_p.conscientiousness, new_p.conscientiousness);
println!(" Extraversion: {:.2}{:.2}", old_p.extraversion, new_p.extraversion);
println!(" Agreeableness: {:.2}{:.2}", old_p.agreeableness, new_p.agreeableness);
println!(" Neuroticism: {:.2}{:.2}", old_p.neuroticism, new_p.neuroticism);
}
}
} else {
output::print_output(&profile, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}

View file

@ -0,0 +1,124 @@
use anyhow::Result;
use crate::api::ApiClient;
use crate::output::{self, OutputFormat};
use crate::ui;
pub fn list(
client: &ApiClient,
agent_id: &str,
query: Option<String>,
limit: i32,
offset: i32,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching documents..."))
} else {
None
};
let response = client.list_documents(agent_id, query.as_deref(), Some(limit), Some(offset), verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(docs_response) => {
if output_format == OutputFormat::Pretty {
ui::print_info(&format!("Documents for agent '{}' (total: {})", agent_id, docs_response.total));
for doc in &docs_response.items {
let id = doc.get("id").and_then(|v| v.as_str()).unwrap_or("unknown");
let created = doc.get("created_at").and_then(|v| v.as_str()).unwrap_or("unknown");
let updated = doc.get("updated_at").and_then(|v| v.as_str()).unwrap_or("unknown");
let text_len = doc.get("text_length").and_then(|v| v.as_i64()).unwrap_or(0);
let mem_count = doc.get("memory_unit_count").and_then(|v| v.as_i64()).unwrap_or(0);
println!("\n Document ID: {}", id);
println!(" Created: {}", created);
println!(" Updated: {}", updated);
println!(" Text Length: {}", text_len);
println!(" Memory Units: {}", mem_count);
}
} else {
output::print_output(&docs_response, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn get(
client: &ApiClient,
agent_id: &str,
document_id: &str,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching document..."))
} else {
None
};
let response = client.get_document(agent_id, document_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(doc) => {
if output_format == OutputFormat::Pretty {
ui::print_info(&format!("Document: {}", doc.id));
println!(" Agent ID: {}", doc.agent_id);
println!(" Created: {}", doc.created_at);
println!(" Updated: {}", doc.updated_at);
println!(" Memory Units: {}", doc.memory_unit_count);
println!("\n Text:\n{}", doc.original_text);
} else {
output::print_output(&doc, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn delete(
client: &ApiClient,
agent_id: &str,
document_id: &str,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Deleting document..."))
} else {
None
};
let response = client.delete_document(agent_id, document_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
if result.success {
ui::print_success("Document deleted successfully");
} else {
ui::print_error("Failed to delete document");
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}

View file

@ -0,0 +1,136 @@
use anyhow::Result;
use crate::api::ApiClient;
use crate::output::{self, OutputFormat};
use crate::ui;
pub fn list(
client: &ApiClient,
bank_id: &str,
limit: i64,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching entities..."))
} else {
None
};
let response = client.list_entities(bank_id, Some(limit), verbose)?;
if let Some(sp) = spinner {
sp.finish_and_clear();
}
if output_format == OutputFormat::Pretty {
ui::print_section_header(&format!("Entities for Bank: {}", bank_id));
if response.entities.is_empty() {
ui::print_warning("No entities found");
return Ok(());
}
println!("Total entities: {}\n", response.entities.len());
for entity in &response.entities {
println!("ID: {}", entity.id);
println!(" Name: {}", entity.canonical_name);
println!(" Mentions: {}", entity.mention_count);
if let Some(first_seen) = &entity.first_seen {
println!(" First seen: {}", first_seen);
}
if let Some(last_seen) = &entity.last_seen {
println!(" Last seen: {}", last_seen);
}
println!();
}
} else {
output::print_output(&response, output_format)?;
}
Ok(())
}
pub fn get(
client: &ApiClient,
bank_id: &str,
entity_id: &str,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching entity details..."))
} else {
None
};
let response = client.get_entity(bank_id, entity_id, verbose)?;
if let Some(sp) = spinner {
sp.finish_and_clear();
}
if output_format == OutputFormat::Pretty {
ui::print_section_header(&format!("Entity: {}", entity_id));
println!("ID: {}", response.id);
println!("Name: {}", response.canonical_name);
println!("Mentions: {}", response.mention_count);
if let Some(first_seen) = &response.first_seen {
println!("First seen: {}", first_seen);
}
if let Some(last_seen) = &response.last_seen {
println!("Last seen: {}", last_seen);
}
// Show observations (always included)
if !response.observations.is_empty() {
println!("\nObservations ({}):", response.observations.len());
for obs in &response.observations {
println!(" - {}", obs.text);
if let Some(mentioned_at) = &obs.mentioned_at {
println!(" Mentioned at: {}", mentioned_at);
}
}
}
println!();
} else {
output::print_output(&response, output_format)?;
}
Ok(())
}
pub fn regenerate(
client: &ApiClient,
bank_id: &str,
entity_id: &str,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Regenerating entity observations..."))
} else {
None
};
let response = client.regenerate_entity(bank_id, entity_id, verbose)?;
if let Some(sp) = spinner {
sp.finish_and_clear();
}
if output_format == OutputFormat::Pretty {
ui::print_success(&format!("Successfully regenerated observations for entity: {}", entity_id));
println!("\nUpdated entity:");
println!(" Name: {}", response.canonical_name);
println!(" Mentions: {}", response.mention_count);
println!(" Observations: {}", response.observations.len());
} else {
output::print_output(&response, output_format)?;
}
Ok(())
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,397 @@
use anyhow::{Context, Result};
use std::fs;
use std::path::PathBuf;
use walkdir::WalkDir;
use crate::api::{ApiClient, RecallRequest, ReflectRequest, MemoryItem, RetainRequest};
use crate::config;
use crate::output::{self, OutputFormat};
use crate::ui;
// Import Budget type from generated client
use hindsight_client::types::Budget;
// Helper function to parse budget string to Budget enum
fn parse_budget(budget: &str) -> Budget {
match budget.to_lowercase().as_str() {
"low" => Budget::Low,
"high" => Budget::High,
_ => Budget::Mid, // Default to mid
}
}
pub fn recall(
client: &ApiClient,
agent_id: &str,
query: String,
fact_type: Vec<String>,
budget: String,
max_tokens: i64,
trace: bool,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Recalling memories..."))
} else {
None
};
let request = RecallRequest {
query,
types: if fact_type.is_empty() { None } else { Some(fact_type) },
budget: Some(parse_budget(&budget)),
max_tokens,
trace,
query_timestamp: None,
filters: None,
include: None,
};
let response = client.recall(agent_id, &request, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
ui::print_search_results(&result, trace);
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn reflect(
client: &ApiClient,
agent_id: &str,
query: String,
budget: String,
context: Option<String>,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Reflecting..."))
} else {
None
};
let request = ReflectRequest {
query,
budget: Some(parse_budget(&budget)),
context,
filters: None,
include: None,
};
let response = client.reflect(agent_id, &request, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
ui::print_think_response(&result);
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn retain(
client: &ApiClient,
agent_id: &str,
content: String,
doc_id: Option<String>,
context: Option<String>,
r#async: bool,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let doc_id = doc_id.unwrap_or_else(config::generate_doc_id);
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Retaining memory..."))
} else {
None
};
let item = MemoryItem {
content: content.clone(),
context,
metadata: None,
timestamp: None,
};
let request = RetainRequest {
items: vec![item],
document_id: Some(doc_id.clone()),
async_: r#async,
};
let response = client.retain(agent_id, &request, r#async, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
ui::print_success(&format!(
"Memory retained successfully (document: {})",
doc_id
));
if result.is_async {
println!(" Status: queued for background processing");
println!(" Items: {}", result.items_count);
} else {
println!(" Stored count: {}", result.items_count);
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn retain_files(
client: &ApiClient,
agent_id: &str,
path: PathBuf,
recursive: bool,
context: Option<String>,
r#async: bool,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
if !path.exists() {
anyhow::bail!("Path does not exist: {}", path.display());
}
let mut files = Vec::new();
if path.is_file() {
files.push(path);
} else if path.is_dir() {
if recursive {
for entry in WalkDir::new(&path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let path = entry.path();
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() {
if let Some(ext) = path.extension() {
if ext == "txt" || ext == "md" {
files.push(path);
}
}
}
}
}
}
if files.is_empty() {
ui::print_warning("No .txt or .md files found");
return Ok(());
}
ui::print_info(&format!("Found {} files to import", files.len()));
let pb = ui::create_progress_bar(files.len() as u64, "Processing files");
let mut items = Vec::new();
let mut document_id = None;
for file_path in &files {
let content = fs::read_to_string(file_path)
.with_context(|| format!("Failed to read file: {}", file_path.display()))?;
let doc_id = file_path
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
.unwrap_or_else(config::generate_doc_id);
if document_id.is_none() {
document_id = Some(doc_id);
}
items.push(MemoryItem {
content,
context: context.clone(),
metadata: None,
timestamp: None,
});
pb.inc(1);
}
pb.finish_with_message("Files processed");
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Retaining memories..."))
} else {
None
};
let request = RetainRequest {
items,
document_id,
async_: r#async,
};
let response = client.retain(agent_id, &request, r#async, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
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);
} else {
println!(" Total units created: {}", result.items_count);
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn delete(
client: &ApiClient,
agent_id: &str,
unit_id: &str,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Deleting memory unit..."))
} else {
None
};
let response = client.delete_memory(agent_id, unit_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
if result.success {
ui::print_success("Memory unit deleted successfully");
} else {
ui::print_error("Failed to delete memory unit");
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn clear(
client: &ApiClient,
agent_id: &str,
fact_type: Option<String>,
yes: bool,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
// Confirmation prompt unless -y flag is used
if !yes && output_format == OutputFormat::Pretty {
let message = if let Some(ft) = &fact_type {
format!(
"Are you sure you want to clear all '{}' memories for agent '{}'? This cannot be undone.",
ft, agent_id
)
} else {
format!(
"Are you sure you want to clear ALL memories for agent '{}'? This cannot be undone.",
agent_id
)
};
let confirmed = ui::prompt_confirmation(&message)?;
if !confirmed {
ui::print_info("Operation cancelled");
return Ok(());
}
}
let spinner_msg = if let Some(ft) = &fact_type {
format!("Clearing {} memories...", ft)
} else {
"Clearing all memories...".to_string()
};
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner(&spinner_msg))
} else {
None
};
let response = client.clear_memories(agent_id, fact_type.as_deref(), verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
if result.success {
let msg = if fact_type.is_some() {
"Memories cleared successfully"
} else {
"All memories cleared successfully"
};
ui::print_success(msg);
} else {
ui::print_error("Failed to clear memories");
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}

View file

@ -0,0 +1,6 @@
pub mod bank;
pub mod memory;
pub mod document;
pub mod entity;
pub mod operation;
pub mod explore;

View file

@ -0,0 +1,84 @@
use anyhow::Result;
use crate::api::ApiClient;
use crate::output::{self, OutputFormat};
use crate::ui;
pub fn list(
client: &ApiClient,
agent_id: &str,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching operations..."))
} else {
None
};
let response = client.list_operations(agent_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(ops_response) => {
if output_format == OutputFormat::Pretty {
if ops_response.operations.is_empty() {
ui::print_info("No operations found");
} else {
ui::print_info(&format!("Found {} operation(s)", ops_response.operations.len()));
for op in &ops_response.operations {
println!("\n Operation ID: {}", op.id);
println!(" Type: {}", op.task_type);
println!(" Status: {}", op.status);
println!(" Items: {}", op.items_count);
if let Some(doc_id) = &op.document_id {
println!(" Document ID: {}", doc_id);
}
}
}
} else {
output::print_output(&ops_response, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}
pub fn cancel(
client: &ApiClient,
agent_id: &str,
operation_id: &str,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Cancelling operation..."))
} else {
None
};
let response = client.cancel_operation(agent_id, operation_id, verbose);
if let Some(sp) = spinner {
sp.finish_and_clear();
}
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
if result.success {
ui::print_success("Operation cancelled successfully");
} else {
ui::print_error("Failed to cancel operation");
}
} else {
output::print_output(&result, output_format)?;
}
Ok(())
}
Err(e) => Err(e)
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
use crate::api::{AgentProfile, Fact, SearchResponse, ThinkResponse, TraceInfo};
use crate::api::{BankProfileResponse, RecallResult, RecallResponse, ReflectResponse};
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use std::io::{self, Write};
@ -9,8 +9,8 @@ pub fn print_section_header(title: &str) {
println!();
}
pub fn print_fact(fact: &Fact, show_activation: bool) {
let fact_type = fact.fact_type.as_deref().unwrap_or("unknown");
pub fn print_fact(fact: &RecallResult, show_activation: bool) {
let fact_type = fact.type_.as_deref().unwrap_or("unknown");
let type_color = match fact_type {
"world" => "cyan",
@ -29,10 +29,10 @@ pub fn print_fact(fact: &Fact, show_activation: bool) {
print!("{} ", prefix);
print!("{}", format!("[{}]", fact_type.to_uppercase()).color(type_color).bold());
// Note: activation field not available in generated SearchResult
// The API doesn't return it in the current schema
if show_activation {
if let Some(activation) = fact.activation {
print!(" {}", format!("({:.2})", activation).bright_black());
}
// Placeholder for when activation is added to the API schema
}
println!();
@ -44,27 +44,12 @@ pub fn print_fact(fact: &Fact, show_activation: bool) {
}
// Show temporal information
// If occurred_start/end exist, show them; otherwise fall back to event_date
if let Some(occurred_start) = &fact.occurred_start {
if let Some(occurred_end) = &fact.occurred_end {
if occurred_start == occurred_end {
// Point event
println!(" {}: {}", "Occurred".bright_black(), occurred_start.bright_black());
println!(" {}: {} - {}", "Date".bright_black(), occurred_start.bright_black(), occurred_end.bright_black());
} else {
// Range event
println!(" {}: {} to {}", "Occurred".bright_black(), occurred_start.bright_black(), occurred_end.bright_black());
println!(" {}: {}", "Date".bright_black(), occurred_start.bright_black());
}
} else {
println!(" {}: {}", "Occurred".bright_black(), occurred_start.bright_black());
}
} else if let Some(event_date) = &fact.event_date {
// Fallback for backward compatibility
println!(" {}: {}", "Date".bright_black(), event_date.bright_black());
}
// Show when fact was mentioned (learned)
if let Some(mentioned_at) = &fact.mentioned_at {
println!(" {}: {}", "Mentioned".bright_black(), mentioned_at.bright_black());
}
// Show document ID if available
@ -75,7 +60,7 @@ pub fn print_fact(fact: &Fact, show_activation: bool) {
println!();
}
pub fn print_search_results(response: &SearchResponse, show_trace: bool) {
pub fn print_search_results(response: &RecallResponse, show_trace: bool) {
let results = &response.results;
print_section_header(&format!("Search Results ({})", results.len()));
@ -95,7 +80,7 @@ pub fn print_search_results(response: &SearchResponse, show_trace: bool) {
}
}
pub fn print_think_response(response: &ThinkResponse) {
pub fn print_think_response(response: &ReflectResponse) {
println!();
println!("{}", response.text.bright_white());
println!();
@ -105,14 +90,14 @@ pub fn print_think_response(response: &ThinkResponse) {
}
}
pub fn print_trace_info(trace: &TraceInfo) {
pub fn print_trace_info(trace: &serde_json::Map<String, serde_json::Value>) {
print_section_header("Trace Information");
if let Some(time) = trace.total_time {
if let Some(time) = trace.get("total_time").and_then(|v| v.as_f64()) {
println!(" ⏱️ Total time: {}", format!("{:.2}ms", time).bright_green());
}
if let Some(count) = trace.activation_count {
if let Some(count) = trace.get("activation_count").and_then(|v| v.as_i64()) {
println!(" 📊 Activation count: {}", count.to_string().bright_green());
}
@ -170,8 +155,8 @@ pub fn prompt_confirmation(message: &str) -> io::Result<bool> {
Ok(input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes"))
}
pub fn print_profile(profile: &AgentProfile) {
print_section_header(&format!("Agent Profile: {}", profile.agent_id));
pub fn print_profile(profile: &BankProfileResponse) {
print_section_header(&format!("Bank Profile: {}", profile.bank_id));
// Print name
println!("{} {}", "Name:".bright_cyan().bold(), profile.name.bright_white());
@ -200,7 +185,7 @@ pub fn print_profile(profile: &AgentProfile) {
for (name, value, emoji, color) in &traits {
let bar_length = 40;
let filled = (*value * bar_length as f32) as usize;
let filled = (*value * bar_length as f64) as usize;
let empty = bar_length - filled;
let bar = format!("{}{}", "".repeat(filled), "".repeat(empty));
@ -224,7 +209,7 @@ pub fn print_profile(profile: &AgentProfile) {
println!("{}", "Bias Strength:".bright_yellow());
let bias = profile.personality.bias_strength;
let bar_length = 40;
let filled = (bias * bar_length as f32) as usize;
let filled = (bias * bar_length as f64) as usize;
let empty = bar_length - filled;
let bar = format!("{}{}", "".repeat(filled), "".repeat(empty));

View file

@ -0,0 +1,15 @@
use anyhow::{Context, Result};
use crate::api::ApiClient;
use crate::config::Config;
use crate::output::OutputFormat;
/// Get API client from config
pub fn get_client(config: &Config) -> Result<ApiClient> {
ApiClient::new(config.api_url.clone())
.context("Failed to create API client")
}
/// Get output format, preferring CLI arg over default
pub fn get_output_format(cli_format: Option<OutputFormat>, _config: &Config) -> OutputFormat {
cli_format.unwrap_or(OutputFormat::Pretty)
}

View file

@ -1,103 +1,118 @@
memora_client_api/__init__.py
memora_client_api/api/__init__.py
memora_client_api/api/agent_management_api.py
memora_client_api/api/documents_api.py
memora_client_api/api/memory_operations_api.py
memora_client_api/api/reasoning_api.py
memora_client_api/api/visualization_api.py
memora_client_api/api_client.py
memora_client_api/api_response.py
memora_client_api/configuration.py
memora_client_api/docs/AddBackgroundRequest.md
memora_client_api/docs/AgentListItem.md
memora_client_api/docs/AgentListResponse.md
memora_client_api/docs/AgentManagementApi.md
memora_client_api/docs/AgentProfileResponse.md
memora_client_api/docs/BackgroundResponse.md
memora_client_api/docs/BatchPutAsyncResponse.md
memora_client_api/docs/BatchPutRequest.md
memora_client_api/docs/BatchPutResponse.md
memora_client_api/docs/CreateAgentRequest.md
memora_client_api/docs/DeleteResponse.md
memora_client_api/docs/DocumentResponse.md
memora_client_api/docs/DocumentsApi.md
memora_client_api/docs/GraphDataResponse.md
memora_client_api/docs/HTTPValidationError.md
memora_client_api/docs/ListDocumentsResponse.md
memora_client_api/docs/ListMemoryUnitsResponse.md
memora_client_api/docs/MemoryItem.md
memora_client_api/docs/MemoryOperationsApi.md
memora_client_api/docs/PersonalityTraits.md
memora_client_api/docs/ReasoningApi.md
memora_client_api/docs/SearchRequest.md
memora_client_api/docs/SearchResponse.md
memora_client_api/docs/SearchResult.md
memora_client_api/docs/ThinkFact.md
memora_client_api/docs/ThinkRequest.md
memora_client_api/docs/ThinkResponse.md
memora_client_api/docs/UpdatePersonalityRequest.md
memora_client_api/docs/ValidationError.md
memora_client_api/docs/ValidationErrorLocInner.md
memora_client_api/docs/VisualizationApi.md
memora_client_api/exceptions.py
memora_client_api/models/__init__.py
memora_client_api/models/add_background_request.py
memora_client_api/models/agent_list_item.py
memora_client_api/models/agent_list_response.py
memora_client_api/models/agent_profile_response.py
memora_client_api/models/background_response.py
memora_client_api/models/batch_put_async_response.py
memora_client_api/models/batch_put_request.py
memora_client_api/models/batch_put_response.py
memora_client_api/models/create_agent_request.py
memora_client_api/models/delete_response.py
memora_client_api/models/document_response.py
memora_client_api/models/graph_data_response.py
memora_client_api/models/http_validation_error.py
memora_client_api/models/list_documents_response.py
memora_client_api/models/list_memory_units_response.py
memora_client_api/models/memory_item.py
memora_client_api/models/personality_traits.py
memora_client_api/models/search_request.py
memora_client_api/models/search_response.py
memora_client_api/models/search_result.py
memora_client_api/models/think_fact.py
memora_client_api/models/think_request.py
memora_client_api/models/think_response.py
memora_client_api/models/update_personality_request.py
memora_client_api/models/validation_error.py
memora_client_api/models/validation_error_loc_inner.py
memora_client_api/rest.py
memora_client_api/test/__init__.py
memora_client_api/test/test_add_background_request.py
memora_client_api/test/test_agent_list_item.py
memora_client_api/test/test_agent_list_response.py
memora_client_api/test/test_agent_management_api.py
memora_client_api/test/test_agent_profile_response.py
memora_client_api/test/test_background_response.py
memora_client_api/test/test_batch_put_async_response.py
memora_client_api/test/test_batch_put_request.py
memora_client_api/test/test_batch_put_response.py
memora_client_api/test/test_create_agent_request.py
memora_client_api/test/test_delete_response.py
memora_client_api/test/test_document_response.py
memora_client_api/test/test_documents_api.py
memora_client_api/test/test_graph_data_response.py
memora_client_api/test/test_http_validation_error.py
memora_client_api/test/test_list_documents_response.py
memora_client_api/test/test_list_memory_units_response.py
memora_client_api/test/test_memory_item.py
memora_client_api/test/test_memory_operations_api.py
memora_client_api/test/test_personality_traits.py
memora_client_api/test/test_reasoning_api.py
memora_client_api/test/test_search_request.py
memora_client_api/test/test_search_response.py
memora_client_api/test/test_search_result.py
memora_client_api/test/test_think_fact.py
memora_client_api/test/test_think_request.py
memora_client_api/test/test_think_response.py
memora_client_api/test/test_update_personality_request.py
memora_client_api/test/test_validation_error.py
memora_client_api/test/test_validation_error_loc_inner.py
memora_client_api/test/test_visualization_api.py
memora_client_api_README.md
hindsight_client_api/__init__.py
hindsight_client_api/api/__init__.py
hindsight_client_api/api/default_api.py
hindsight_client_api/api_client.py
hindsight_client_api/api_response.py
hindsight_client_api/configuration.py
hindsight_client_api/docs/AddBackgroundRequest.md
hindsight_client_api/docs/BackgroundResponse.md
hindsight_client_api/docs/BankListItem.md
hindsight_client_api/docs/BankListResponse.md
hindsight_client_api/docs/BankProfileResponse.md
hindsight_client_api/docs/Budget.md
hindsight_client_api/docs/CreateBankRequest.md
hindsight_client_api/docs/DefaultApi.md
hindsight_client_api/docs/DeleteResponse.md
hindsight_client_api/docs/DocumentResponse.md
hindsight_client_api/docs/EntityDetailResponse.md
hindsight_client_api/docs/EntityIncludeOptions.md
hindsight_client_api/docs/EntityListItem.md
hindsight_client_api/docs/EntityListResponse.md
hindsight_client_api/docs/EntityObservationResponse.md
hindsight_client_api/docs/EntityStateResponse.md
hindsight_client_api/docs/GraphDataResponse.md
hindsight_client_api/docs/HTTPValidationError.md
hindsight_client_api/docs/IncludeOptions.md
hindsight_client_api/docs/ListDocumentsResponse.md
hindsight_client_api/docs/ListMemoryUnitsResponse.md
hindsight_client_api/docs/MemoryItem.md
hindsight_client_api/docs/MetadataFilter.md
hindsight_client_api/docs/PersonalityTraits.md
hindsight_client_api/docs/RecallRequest.md
hindsight_client_api/docs/RecallResponse.md
hindsight_client_api/docs/RecallResult.md
hindsight_client_api/docs/ReflectFact.md
hindsight_client_api/docs/ReflectIncludeOptions.md
hindsight_client_api/docs/ReflectRequest.md
hindsight_client_api/docs/ReflectResponse.md
hindsight_client_api/docs/RetainRequest.md
hindsight_client_api/docs/RetainResponse.md
hindsight_client_api/docs/UpdatePersonalityRequest.md
hindsight_client_api/docs/ValidationError.md
hindsight_client_api/docs/ValidationErrorLocInner.md
hindsight_client_api/exceptions.py
hindsight_client_api/models/__init__.py
hindsight_client_api/models/add_background_request.py
hindsight_client_api/models/background_response.py
hindsight_client_api/models/bank_list_item.py
hindsight_client_api/models/bank_list_response.py
hindsight_client_api/models/bank_profile_response.py
hindsight_client_api/models/budget.py
hindsight_client_api/models/create_bank_request.py
hindsight_client_api/models/delete_response.py
hindsight_client_api/models/document_response.py
hindsight_client_api/models/entity_detail_response.py
hindsight_client_api/models/entity_include_options.py
hindsight_client_api/models/entity_list_item.py
hindsight_client_api/models/entity_list_response.py
hindsight_client_api/models/entity_observation_response.py
hindsight_client_api/models/entity_state_response.py
hindsight_client_api/models/graph_data_response.py
hindsight_client_api/models/http_validation_error.py
hindsight_client_api/models/include_options.py
hindsight_client_api/models/list_documents_response.py
hindsight_client_api/models/list_memory_units_response.py
hindsight_client_api/models/memory_item.py
hindsight_client_api/models/metadata_filter.py
hindsight_client_api/models/personality_traits.py
hindsight_client_api/models/recall_request.py
hindsight_client_api/models/recall_response.py
hindsight_client_api/models/recall_result.py
hindsight_client_api/models/reflect_fact.py
hindsight_client_api/models/reflect_include_options.py
hindsight_client_api/models/reflect_request.py
hindsight_client_api/models/reflect_response.py
hindsight_client_api/models/retain_request.py
hindsight_client_api/models/retain_response.py
hindsight_client_api/models/update_personality_request.py
hindsight_client_api/models/validation_error.py
hindsight_client_api/models/validation_error_loc_inner.py
hindsight_client_api/rest.py
hindsight_client_api/test/__init__.py
hindsight_client_api/test/test_add_background_request.py
hindsight_client_api/test/test_background_response.py
hindsight_client_api/test/test_bank_list_item.py
hindsight_client_api/test/test_bank_list_response.py
hindsight_client_api/test/test_bank_profile_response.py
hindsight_client_api/test/test_budget.py
hindsight_client_api/test/test_create_bank_request.py
hindsight_client_api/test/test_default_api.py
hindsight_client_api/test/test_delete_response.py
hindsight_client_api/test/test_document_response.py
hindsight_client_api/test/test_entity_detail_response.py
hindsight_client_api/test/test_entity_include_options.py
hindsight_client_api/test/test_entity_list_item.py
hindsight_client_api/test/test_entity_list_response.py
hindsight_client_api/test/test_entity_observation_response.py
hindsight_client_api/test/test_entity_state_response.py
hindsight_client_api/test/test_graph_data_response.py
hindsight_client_api/test/test_http_validation_error.py
hindsight_client_api/test/test_include_options.py
hindsight_client_api/test/test_list_documents_response.py
hindsight_client_api/test/test_list_memory_units_response.py
hindsight_client_api/test/test_memory_item.py
hindsight_client_api/test/test_metadata_filter.py
hindsight_client_api/test/test_personality_traits.py
hindsight_client_api/test/test_recall_request.py
hindsight_client_api/test/test_recall_response.py
hindsight_client_api/test/test_recall_result.py
hindsight_client_api/test/test_reflect_fact.py
hindsight_client_api/test/test_reflect_include_options.py
hindsight_client_api/test/test_reflect_request.py
hindsight_client_api/test/test_reflect_response.py
hindsight_client_api/test/test_retain_request.py
hindsight_client_api/test/test_retain_response.py
hindsight_client_api/test/test_update_personality_request.py
hindsight_client_api/test/test_validation_error.py
hindsight_client_api/test/test_validation_error_loc_inner.py
hindsight_client_api_README.md

View file

@ -1,134 +0,0 @@
# Memora Python Client
Clean, pythonic client for the Memora API - A semantic memory system with personality-driven thinking.
## Installation
```bash
pip install memora-client
```
## Quick Start
```python
from memora_client import Memora
# Initialize client
client = Memora(base_url="http://localhost:8888")
# Store a memory
client.store(agent_id="alice", content="Alice loves artificial intelligence")
# Search memories
results = client.search(agent_id="alice", query="What does Alice like?")
print(results)
# Generate contextual answer
answer = client.think(agent_id="alice", query="What are my interests?")
print(answer["text"])
```
## Main Operations
### Store Memories
```python
# Store a single memory
client.store(
agent_id="alice",
content="Alice completed a Python project using FastAPI",
event_date=datetime(2024, 1, 15),
context="work projects"
)
# Store multiple memories in batch
client.store_batch(
agent_id="alice",
items=[
{"content": "Alice loves machine learning"},
{"content": "Bob enjoys hiking", "event_date": datetime(2024, 10, 15)},
]
)
```
### Search Memories
```python
# Simple search
results = client.search(
agent_id="alice",
query="What does Alice like?",
max_tokens=2048
)
# Advanced search with all options
response = client.search_memories(
agent_id="alice",
query="What are Alice's interests?",
fact_type=["world"],
max_tokens=4096,
trace=True # Include trace information
)
```
### Think (Generate Contextual Answers)
```python
answer = client.think(
agent_id="alice",
query="What should I focus on learning next?",
thinking_budget=100,
context="I want to advance my career in AI"
)
print(answer["text"]) # The generated answer
print(answer["based_on"]) # Facts used to generate the answer
```
## Structure
```
memora-client/
├── memora_client/ # Maintained wrapper (simple API)
│ ├── __init__.py
│ ├── memora_client.py # Clean interface: store(), search(), think()
│ └── tests/
│ └── test_main_operations.py
└── hindsight_client_api/ # Auto-generated from OpenAPI spec
├── api/ # Full API operations
├── models/ # Request/response models
└── ...
```
## Testing
Run integration tests (requires running Memora API server):
```bash
# Set API URL (optional, defaults to http://localhost:8888)
export MEMORA_API_URL=http://localhost:8888
# Run tests
pytest memora_client/tests/test_main_operations.py -v
```
## Development
### Regenerate Client
The low-level API client is auto-generated from the OpenAPI spec. The high-level wrapper (`memora_client/`) is maintained and won't be overwritten.
```bash
# Regenerate from OpenAPI spec
./scripts/generate-clients.sh
```
This preserves:
- `memora_client/` - Maintained wrapper
- `pyproject.toml` - Package configuration
- Tests and documentation
## License
Apache 2.0

View file

@ -10,12 +10,12 @@ from typing import Optional, List, Dict, Any
from datetime import datetime
import hindsight_client_api
from hindsight_client_api.api import memory_operations_api, reasoning_api, agent_management_api
from hindsight_client_api.api import default_api
from hindsight_client_api.models import (
search_request,
batch_put_request,
recall_request,
retain_request,
memory_item,
think_request,
reflect_request,
)
@ -41,13 +41,13 @@ class Hindsight:
client = Hindsight(base_url="http://localhost:8888")
# Store a memory
client.put(agent_id="alice", content="Alice loves AI")
client.retain(bank_id="alice", content="Alice loves AI")
# Search memories
results = client.search(agent_id="alice", query="What does Alice like?")
# Recall memories
results = client.recall(bank_id="alice", query="What does Alice like?")
# Generate contextual answer
answer = client.think(agent_id="alice", query="What are my interests?")
answer = client.reflect(bank_id="alice", query="What are my interests?")
```
"""
@ -61,9 +61,7 @@ class Hindsight:
"""
config = hindsight_client_api.Configuration(host=base_url)
self._api_client = hindsight_client_api.ApiClient(config)
self._memory_api = memory_operations_api.MemoryOperationsApi(self._api_client)
self._reasoning_api = reasoning_api.ReasoningApi(self._api_client)
self._agent_api = agent_management_api.AgentManagementApi(self._api_client)
self._api = default_api.DefaultApi(self._api_client)
def __enter__(self):
"""Context manager entry."""
@ -80,46 +78,50 @@ class Hindsight:
# Simplified methods for main operations
def put(
def retain(
self,
agent_id: str,
bank_id: str,
content: str,
event_date: Optional[datetime] = None,
timestamp: Optional[datetime] = None,
context: Optional[str] = None,
document_id: Optional[str] = None,
metadata: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""
Store a single memory (simplified interface).
Args:
agent_id: The agent ID
bank_id: The memory bank ID
content: Memory content
event_date: Optional event timestamp
timestamp: Optional event timestamp
context: Optional context description
document_id: Optional document ID for grouping
metadata: Optional user-defined metadata
Returns:
Response with success status
"""
return self.put_batch(
agent_id=agent_id,
items=[{"content": content, "event_date": event_date, "context": context}],
return self.retain_batch(
bank_id=bank_id,
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata}],
document_id=document_id,
)
def put_batch(
def retain_batch(
self,
agent_id: str,
bank_id: str,
items: List[Dict[str, Any]],
document_id: Optional[str] = None,
async_: bool = False,
) -> Dict[str, Any]:
"""
Store multiple memories in batch.
Args:
agent_id: The agent ID
items: List of memory items with 'content' and optional 'event_date', 'context'
bank_id: The memory bank ID
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata'
document_id: Optional document ID for grouping memories
async_: If True, process asynchronously in background (default: False)
Returns:
Response with success status and item count
@ -127,172 +129,193 @@ class Hindsight:
memory_items = [
memory_item.MemoryItem(
content=item["content"],
event_date=item.get("event_date"),
timestamp=item.get("timestamp"),
context=item.get("context"),
metadata=item.get("metadata"),
)
for item in items
]
request_obj = batch_put_request.BatchPutRequest(
request_obj = retain_request.RetainRequest(
items=memory_items,
document_id=document_id,
async_=async_,
)
response = _run_async(self._memory_api.batch_put_memories(agent_id, request_obj))
response = _run_async(self._api.retain_memories(bank_id, request_obj))
return response.to_dict() if hasattr(response, 'to_dict') else response
def search(
def recall(
self,
agent_id: str,
bank_id: str,
query: str,
fact_type: Optional[List[str]] = None,
types: Optional[List[str]] = None,
max_tokens: int = 4096,
thinking_budget: int = 100,
budget: str = "mid",
) -> List[Dict[str, Any]]:
"""
Search memories using semantic similarity.
Recall memories using semantic similarity.
Args:
agent_id: The agent ID
bank_id: The memory bank ID
query: Search query
fact_type: Optional list of fact types to filter (world, agent, opinion)
types: Optional list of fact types to filter (world, agent, opinion, observation)
max_tokens: Maximum tokens in results (default: 4096)
thinking_budget: Token budget for search (default: 100)
budget: Budget level for recall - "low", "mid", or "high" (default: "mid")
Returns:
List of search results
List of recall results
"""
request_obj = search_request.SearchRequest(
request_obj = recall_request.RecallRequest(
query=query,
fact_type=fact_type,
thinking_budget=thinking_budget,
types=types,
budget=budget,
max_tokens=max_tokens,
trace=False,
)
response = _run_async(self._memory_api.search_memories(agent_id, request_obj))
response = _run_async(self._api.recall_memories(bank_id, request_obj))
if hasattr(response, 'results'):
return [r.to_dict() if hasattr(r, 'to_dict') else r for r in response.results]
return []
def think(
def reflect(
self,
agent_id: str,
bank_id: str,
query: str,
thinking_budget: int = 50,
budget: str = "low",
context: Optional[str] = None,
) -> Dict[str, Any]:
"""
Generate a contextual answer based on agent identity and memories.
Generate a contextual answer based on bank identity and memories.
Args:
agent_id: The agent ID
bank_id: The memory bank ID
query: The question or prompt
thinking_budget: Token budget for thinking (default: 50)
budget: Budget level for reflection - "low", "mid", or "high" (default: "low")
context: Optional additional context
Returns:
Response with answer text, facts used, and new opinions
Response with answer text and optionally facts used
"""
request_obj = think_request.ThinkRequest(
request_obj = reflect_request.ReflectRequest(
query=query,
thinking_budget=thinking_budget,
budget=budget,
context=context,
)
response = _run_async(self._reasoning_api.think(agent_id, request_obj))
response = _run_async(self._api.reflect(bank_id, request_obj))
return response.to_dict() if hasattr(response, 'to_dict') else response
# Full-featured methods (expose more options)
def search_memories(
def recall_memories(
self,
agent_id: str,
bank_id: str,
query: str,
fact_type: Optional[List[str]] = None,
thinking_budget: int = 100,
types: Optional[List[str]] = None,
budget: str = "mid",
max_tokens: int = 4096,
trace: bool = False,
question_date: Optional[str] = None,
query_timestamp: Optional[str] = None,
include_entities: bool = True,
max_entity_tokens: int = 500,
) -> Dict[str, Any]:
"""
Search memories with all options (full-featured).
Recall memories with all options (full-featured).
Args:
agent_id: The agent ID
bank_id: The memory bank ID
query: Search query
fact_type: Optional list of fact types to filter
thinking_budget: Token budget for thinking
types: Optional list of fact types to filter (world, agent, opinion, observation)
budget: Budget level - "low", "mid", or "high"
max_tokens: Maximum tokens in results
trace: Enable trace output
question_date: Optional ISO format date string
query_timestamp: Optional ISO format date string (e.g., '2023-05-30T23:40:00')
include_entities: Include entity observations in results (default: True)
max_entity_tokens: Maximum tokens for entity observations (default: 500)
Returns:
Full search response with results and optional trace
Full recall response with results, optional entities, and optional trace
"""
request_obj = search_request.SearchRequest(
query=query,
fact_type=fact_type,
thinking_budget=thinking_budget,
max_tokens=max_tokens,
trace=trace,
question_date=question_date,
from hindsight_client_api.models import include_options, entity_include_options
include_opts = include_options.IncludeOptions(
entities=entity_include_options.EntityIncludeOptions(max_tokens=max_entity_tokens) if include_entities else None
)
response = _run_async(self._memory_api.search_memories(agent_id, request_obj))
request_obj = recall_request.RecallRequest(
query=query,
types=types,
budget=budget,
max_tokens=max_tokens,
trace=trace,
query_timestamp=query_timestamp,
include=include_opts,
)
response = _run_async(self._api.recall_memories(bank_id, request_obj))
return response.to_dict() if hasattr(response, 'to_dict') else response
def list_memories(
self,
agent_id: str,
fact_type: Optional[str] = None,
bank_id: str,
type: Optional[str] = None,
search_query: Optional[str] = None,
limit: int = 100,
offset: int = 0,
) -> Dict[str, Any]:
"""List memory units with pagination."""
response = _run_async(self._memory_api.list_memories(
agent_id=agent_id,
fact_type=fact_type,
response = _run_async(self._api.list_memories(
bank_id=bank_id,
type=type,
q=search_query,
limit=limit,
offset=offset,
))
return response.to_dict() if hasattr(response, 'to_dict') else response
def create_agent(
def create_bank(
self,
agent_id: str,
bank_id: str,
name: Optional[str] = None,
background: Optional[str] = None,
personality: Optional[Dict[str, float]] = None,
) -> Dict[str, Any]:
"""Create or update an agent."""
from hindsight_client_api.models import create_agent_request
"""Create or update a memory bank."""
from hindsight_client_api.models import create_bank_request, personality_traits
request_obj = create_agent_request.CreateAgentRequest(
personality_obj = None
if personality:
personality_obj = personality_traits.PersonalityTraits(**personality)
request_obj = create_bank_request.CreateBankRequest(
name=name,
background=background,
personality=personality_obj,
)
response = _run_async(self._agent_api.create_or_update_agent(agent_id, request_obj))
response = _run_async(self._api.create_or_update_bank(bank_id, request_obj))
return response.to_dict() if hasattr(response, 'to_dict') else response
# Async methods (native async, no _run_async wrapper)
async def aput_batch(
async def aretain_batch(
self,
agent_id: str,
bank_id: str,
items: List[Dict[str, Any]],
document_id: Optional[str] = None,
async_: bool = False,
) -> Dict[str, Any]:
"""
Store multiple memories in batch (async).
Args:
agent_id: The agent ID
items: List of memory items with 'content' and optional 'event_date', 'context'
bank_id: The memory bank ID
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata'
document_id: Optional document ID for grouping memories
async_: If True, process asynchronously in background (default: False)
Returns:
Response with success status and item count
@ -300,110 +323,110 @@ class Hindsight:
memory_items = [
memory_item.MemoryItem(
content=item["content"],
event_date=item.get("event_date"),
timestamp=item.get("timestamp"),
context=item.get("context"),
metadata=item.get("metadata"),
)
for item in items
]
request_obj = batch_put_request.BatchPutRequest(
request_obj = retain_request.RetainRequest(
items=memory_items,
document_id=document_id,
async_=async_,
)
response = await self._memory_api.batch_put_memories(agent_id, request_obj)
response = await self._api.retain_memories(bank_id, request_obj)
return response.to_dict() if hasattr(response, 'to_dict') else response
async def aput(
async def aretain(
self,
agent_id: str,
bank_id: str,
content: str,
event_date: Optional[datetime] = None,
timestamp: Optional[datetime] = None,
context: Optional[str] = None,
document_id: Optional[str] = None,
metadata: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""
Store a single memory (async).
Args:
agent_id: The agent ID
bank_id: The memory bank ID
content: Memory content
event_date: Optional event timestamp
timestamp: Optional event timestamp
context: Optional context description
document_id: Optional document ID for grouping
metadata: Optional user-defined metadata
Returns:
Response with success status
"""
return await self.aput_batch(
agent_id=agent_id,
items=[{"content": content, "event_date": event_date, "context": context}],
return await self.aretain_batch(
bank_id=bank_id,
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata}],
document_id=document_id,
)
async def asearch(
async def arecall(
self,
agent_id: str,
bank_id: str,
query: str,
fact_type: Optional[List[str]] = None,
types: Optional[List[str]] = None,
max_tokens: int = 4096,
thinking_budget: int = 100,
budget: str = "mid",
) -> List[Dict[str, Any]]:
"""
Search memories using semantic similarity (async).
Recall memories using semantic similarity (async).
Args:
agent_id: The agent ID
bank_id: The memory bank ID
query: Search query
fact_type: Optional list of fact types to filter (world, agent, opinion)
types: Optional list of fact types to filter (world, agent, opinion, observation)
max_tokens: Maximum tokens in results (default: 4096)
thinking_budget: Token budget for search (default: 100)
budget: Budget level for recall - "low", "mid", or "high" (default: "mid")
Returns:
List of search results
List of recall results
"""
request_obj = search_request.SearchRequest(
request_obj = recall_request.RecallRequest(
query=query,
fact_type=fact_type,
thinking_budget=thinking_budget,
types=types,
budget=budget,
max_tokens=max_tokens,
trace=False,
)
response = await self._memory_api.search_memories(agent_id, request_obj)
response = await self._api.recall_memories(bank_id, request_obj)
if hasattr(response, 'results'):
return [r.to_dict() if hasattr(r, 'to_dict') else r for r in response.results]
return []
async def athink(
async def areflect(
self,
agent_id: str,
bank_id: str,
query: str,
thinking_budget: int = 50,
budget: str = "low",
context: Optional[str] = None,
) -> Dict[str, Any]:
"""
Generate a contextual answer based on agent identity and memories (async).
Generate a contextual answer based on bank identity and memories (async).
Args:
agent_id: The agent ID
bank_id: The memory bank ID
query: The question or prompt
thinking_budget: Token budget for thinking (default: 50)
budget: Budget level for reflection - "low", "mid", or "high" (default: "low")
context: Optional additional context
Returns:
Response with answer text, facts used, and new opinions
Response with answer text and optionally facts used
"""
request_obj = think_request.ThinkRequest(
request_obj = reflect_request.ReflectRequest(
query=query,
thinking_budget=thinking_budget,
budget=budget,
context=context,
)
response = await self._reasoning_api.think(agent_id, request_obj)
response = await self._api.reflect(bank_id, request_obj)
return response.to_dict() if hasattr(response, 'to_dict') else response
# Alias for backward compatibility
HindsightClient = Hindsight

View file

@ -3,9 +3,9 @@
# flake8: noqa
"""
Agent Memory API
Hindsight HTTP API
A temporal-semantic memory system for AI agents that stores, retrieves, and reasons over memories. ## Features * **Batch Memory Storage**: Store multiple memories efficiently with automatic fact extraction * **Semantic Search**: Find relevant memories using natural language queries * **Fact Type Filtering**: Search across world facts, agent actions, and opinions separately * **Think Endpoint**: Generate contextual answers based on agent identity and memories * **Graph Visualization**: Interactive memory graph visualization * **Document Tracking**: Track and manage memory documents with upsert support ## Architecture The system uses: - **Temporal Links**: Connect memories that are close in time - **Semantic Links**: Connect semantically similar memories - **Entity Links**: Connect memories that mention the same entities - **Spreading Activation**: Intelligent traversal for memory retrieval
HTTP API for Hindsight
The version of the OpenAPI document: 1.0.0
Generated by OpenAPI Generator (https://openapi-generator.tech)
@ -18,11 +18,7 @@ __version__ = "0.0.7"
# Define package exports
__all__ = [
"AgentManagementApi",
"DocumentsApi",
"MemoryOperationsApi",
"ReasoningApi",
"VisualizationApi",
"DefaultApi",
"ApiResponse",
"ApiClient",
"Configuration",
@ -33,39 +29,44 @@ __all__ = [
"ApiAttributeError",
"ApiException",
"AddBackgroundRequest",
"AgentListItem",
"AgentListResponse",
"AgentProfileResponse",
"BackgroundResponse",
"BatchPutAsyncResponse",
"BatchPutRequest",
"BatchPutResponse",
"CreateAgentRequest",
"BankListItem",
"BankListResponse",
"BankProfileResponse",
"Budget",
"CreateBankRequest",
"DeleteResponse",
"DocumentResponse",
"EntityDetailResponse",
"EntityIncludeOptions",
"EntityListItem",
"EntityListResponse",
"EntityObservationResponse",
"EntityStateResponse",
"GraphDataResponse",
"HTTPValidationError",
"IncludeOptions",
"ListDocumentsResponse",
"ListMemoryUnitsResponse",
"MemoryItem",
"MetadataFilter",
"PersonalityTraits",
"SearchRequest",
"SearchResponse",
"SearchResult",
"ThinkFact",
"ThinkRequest",
"ThinkResponse",
"RecallRequest",
"RecallResponse",
"RecallResult",
"ReflectFact",
"ReflectIncludeOptions",
"ReflectRequest",
"ReflectResponse",
"RetainRequest",
"RetainResponse",
"UpdatePersonalityRequest",
"ValidationError",
"ValidationErrorLocInner",
]
# import apis into sdk package
from hindsight_client_api.api.agent_management_api import AgentManagementApi as AgentManagementApi
from hindsight_client_api.api.documents_api import DocumentsApi as DocumentsApi
from hindsight_client_api.api.memory_operations_api import MemoryOperationsApi as MemoryOperationsApi
from hindsight_client_api.api.reasoning_api import ReasoningApi as ReasoningApi
from hindsight_client_api.api.visualization_api import VisualizationApi as VisualizationApi
from hindsight_client_api.api.default_api import DefaultApi as DefaultApi
# import ApiClient
from hindsight_client_api.api_response import ApiResponse as ApiResponse
@ -80,28 +81,37 @@ from hindsight_client_api.exceptions import ApiException as ApiException
# import models into sdk package
from hindsight_client_api.models.add_background_request import AddBackgroundRequest as AddBackgroundRequest
from hindsight_client_api.models.agent_list_item import AgentListItem as AgentListItem
from hindsight_client_api.models.agent_list_response import AgentListResponse as AgentListResponse
from hindsight_client_api.models.agent_profile_response import AgentProfileResponse as AgentProfileResponse
from hindsight_client_api.models.background_response import BackgroundResponse as BackgroundResponse
from hindsight_client_api.models.batch_put_async_response import BatchPutAsyncResponse as BatchPutAsyncResponse
from hindsight_client_api.models.batch_put_request import BatchPutRequest as BatchPutRequest
from hindsight_client_api.models.batch_put_response import BatchPutResponse as BatchPutResponse
from hindsight_client_api.models.create_agent_request import CreateAgentRequest as CreateAgentRequest
from hindsight_client_api.models.bank_list_item import BankListItem as BankListItem
from hindsight_client_api.models.bank_list_response import BankListResponse as BankListResponse
from hindsight_client_api.models.bank_profile_response import BankProfileResponse as BankProfileResponse
from hindsight_client_api.models.budget import Budget as Budget
from hindsight_client_api.models.create_bank_request import CreateBankRequest as CreateBankRequest
from hindsight_client_api.models.delete_response import DeleteResponse as DeleteResponse
from hindsight_client_api.models.document_response import DocumentResponse as DocumentResponse
from hindsight_client_api.models.entity_detail_response import EntityDetailResponse as EntityDetailResponse
from hindsight_client_api.models.entity_include_options import EntityIncludeOptions as EntityIncludeOptions
from hindsight_client_api.models.entity_list_item import EntityListItem as EntityListItem
from hindsight_client_api.models.entity_list_response import EntityListResponse as EntityListResponse
from hindsight_client_api.models.entity_observation_response import EntityObservationResponse as EntityObservationResponse
from hindsight_client_api.models.entity_state_response import EntityStateResponse as EntityStateResponse
from hindsight_client_api.models.graph_data_response import GraphDataResponse as GraphDataResponse
from hindsight_client_api.models.http_validation_error import HTTPValidationError as HTTPValidationError
from hindsight_client_api.models.include_options import IncludeOptions as IncludeOptions
from hindsight_client_api.models.list_documents_response import ListDocumentsResponse as ListDocumentsResponse
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse as ListMemoryUnitsResponse
from hindsight_client_api.models.memory_item import MemoryItem as MemoryItem
from hindsight_client_api.models.metadata_filter import MetadataFilter as MetadataFilter
from hindsight_client_api.models.personality_traits import PersonalityTraits as PersonalityTraits
from hindsight_client_api.models.search_request import SearchRequest as SearchRequest
from hindsight_client_api.models.search_response import SearchResponse as SearchResponse
from hindsight_client_api.models.search_result import SearchResult as SearchResult
from hindsight_client_api.models.think_fact import ThinkFact as ThinkFact
from hindsight_client_api.models.think_request import ThinkRequest as ThinkRequest
from hindsight_client_api.models.think_response import ThinkResponse as ThinkResponse
from hindsight_client_api.models.recall_request import RecallRequest as RecallRequest
from hindsight_client_api.models.recall_response import RecallResponse as RecallResponse
from hindsight_client_api.models.recall_result import RecallResult as RecallResult
from hindsight_client_api.models.reflect_fact import ReflectFact as ReflectFact
from hindsight_client_api.models.reflect_include_options import ReflectIncludeOptions as ReflectIncludeOptions
from hindsight_client_api.models.reflect_request import ReflectRequest as ReflectRequest
from hindsight_client_api.models.reflect_response import ReflectResponse as ReflectResponse
from hindsight_client_api.models.retain_request import RetainRequest as RetainRequest
from hindsight_client_api.models.retain_response import RetainResponse as RetainResponse
from hindsight_client_api.models.update_personality_request import UpdatePersonalityRequest as UpdatePersonalityRequest
from hindsight_client_api.models.validation_error import ValidationError as ValidationError
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner as ValidationErrorLocInner

View file

@ -1,9 +1,5 @@
# flake8: noqa
# import apis into api package
from hindsight_client_api.api.agent_management_api import AgentManagementApi
from hindsight_client_api.api.documents_api import DocumentsApi
from hindsight_client_api.api.memory_operations_api import MemoryOperationsApi
from hindsight_client_api.api.reasoning_api import ReasoningApi
from hindsight_client_api.api.visualization_api import VisualizationApi
from hindsight_client_api.api.default_api import DefaultApi

File diff suppressed because it is too large Load diff

View file

@ -1,909 +0,0 @@
# coding: utf-8
"""
Agent Memory API
A temporal-semantic memory system for AI agents that stores, retrieves, and reasons over memories. ## Features * **Batch Memory Storage**: Store multiple memories efficiently with automatic fact extraction * **Semantic Search**: Find relevant memories using natural language queries * **Fact Type Filtering**: Search across world facts, agent actions, and opinions separately * **Think Endpoint**: Generate contextual answers based on agent identity and memories * **Graph Visualization**: Interactive memory graph visualization * **Document Tracking**: Track and manage memory documents with upsert support ## Architecture The system uses: - **Temporal Links**: Connect memories that are close in time - **Semantic Links**: Connect semantically similar memories - **Entity Links**: Connect memories that mention the same entities - **Spreading Activation**: Intelligent traversal for memory retrieval
The version of the OpenAPI document: 1.0.0
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
import warnings
from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt
from typing import Any, Dict, List, Optional, Tuple, Union
from typing_extensions import Annotated
from pydantic import StrictInt, StrictStr
from typing import Any, Optional
from hindsight_client_api.models.document_response import DocumentResponse
from hindsight_client_api.models.list_documents_response import ListDocumentsResponse
from hindsight_client_api.api_client import ApiClient, RequestSerialized
from hindsight_client_api.api_response import ApiResponse
from hindsight_client_api.rest import RESTResponseType
class DocumentsApi:
"""NOTE: This class is auto generated by OpenAPI Generator
Ref: https://openapi-generator.tech
Do not edit the class manually.
"""
def __init__(self, api_client=None) -> None:
if api_client is None:
api_client = ApiClient.get_default()
self.api_client = api_client
@validate_call
async def delete_document(
self,
agent_id: StrictStr,
document_id: StrictStr,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> object:
"""Delete a document
Delete a document and all its associated memory units and links. This will cascade delete: - The document itself - All memory units extracted from this document - All links (temporal, semantic, entity) associated with those memory units This operation cannot be undone.
:param agent_id: (required)
:type agent_id: str
:param document_id: (required)
:type document_id: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._delete_document_serialize(
agent_id=agent_id,
document_id=document_id,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "object",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
).data
@validate_call
async def delete_document_with_http_info(
self,
agent_id: StrictStr,
document_id: StrictStr,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[object]:
"""Delete a document
Delete a document and all its associated memory units and links. This will cascade delete: - The document itself - All memory units extracted from this document - All links (temporal, semantic, entity) associated with those memory units This operation cannot be undone.
:param agent_id: (required)
:type agent_id: str
:param document_id: (required)
:type document_id: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._delete_document_serialize(
agent_id=agent_id,
document_id=document_id,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "object",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
)
@validate_call
async def delete_document_without_preload_content(
self,
agent_id: StrictStr,
document_id: StrictStr,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> RESTResponseType:
"""Delete a document
Delete a document and all its associated memory units and links. This will cascade delete: - The document itself - All memory units extracted from this document - All links (temporal, semantic, entity) associated with those memory units This operation cannot be undone.
:param agent_id: (required)
:type agent_id: str
:param document_id: (required)
:type document_id: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._delete_document_serialize(
agent_id=agent_id,
document_id=document_id,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "object",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
return response_data.response
def _delete_document_serialize(
self,
agent_id,
document_id,
_request_auth,
_content_type,
_headers,
_host_index,
) -> RequestSerialized:
_host = None
_collection_formats: Dict[str, str] = {
}
_path_params: Dict[str, str] = {}
_query_params: List[Tuple[str, str]] = []
_header_params: Dict[str, Optional[str]] = _headers or {}
_form_params: List[Tuple[str, str]] = []
_files: Dict[
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
] = {}
_body_params: Optional[bytes] = None
# process the path parameters
if agent_id is not None:
_path_params['agent_id'] = agent_id
if document_id is not None:
_path_params['document_id'] = document_id
# process the query parameters
# process the header parameters
# process the form parameters
# process the body parameter
# set the HTTP header `Accept`
if 'Accept' not in _header_params:
_header_params['Accept'] = self.api_client.select_header_accept(
[
'application/json'
]
)
# authentication setting
_auth_settings: List[str] = [
]
return self.api_client.param_serialize(
method='DELETE',
resource_path='/api/v1/agents/{agent_id}/documents/{document_id}',
path_params=_path_params,
query_params=_query_params,
header_params=_header_params,
body=_body_params,
post_params=_form_params,
files=_files,
auth_settings=_auth_settings,
collection_formats=_collection_formats,
_host=_host,
_request_auth=_request_auth
)
@validate_call
async def get_document(
self,
agent_id: StrictStr,
document_id: StrictStr,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> DocumentResponse:
"""Get document details
Get a specific document including its original text
:param agent_id: (required)
:type agent_id: str
:param document_id: (required)
:type document_id: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._get_document_serialize(
agent_id=agent_id,
document_id=document_id,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "DocumentResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
).data
@validate_call
async def get_document_with_http_info(
self,
agent_id: StrictStr,
document_id: StrictStr,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[DocumentResponse]:
"""Get document details
Get a specific document including its original text
:param agent_id: (required)
:type agent_id: str
:param document_id: (required)
:type document_id: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._get_document_serialize(
agent_id=agent_id,
document_id=document_id,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "DocumentResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
)
@validate_call
async def get_document_without_preload_content(
self,
agent_id: StrictStr,
document_id: StrictStr,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> RESTResponseType:
"""Get document details
Get a specific document including its original text
:param agent_id: (required)
:type agent_id: str
:param document_id: (required)
:type document_id: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._get_document_serialize(
agent_id=agent_id,
document_id=document_id,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "DocumentResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
return response_data.response
def _get_document_serialize(
self,
agent_id,
document_id,
_request_auth,
_content_type,
_headers,
_host_index,
) -> RequestSerialized:
_host = None
_collection_formats: Dict[str, str] = {
}
_path_params: Dict[str, str] = {}
_query_params: List[Tuple[str, str]] = []
_header_params: Dict[str, Optional[str]] = _headers or {}
_form_params: List[Tuple[str, str]] = []
_files: Dict[
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
] = {}
_body_params: Optional[bytes] = None
# process the path parameters
if agent_id is not None:
_path_params['agent_id'] = agent_id
if document_id is not None:
_path_params['document_id'] = document_id
# process the query parameters
# process the header parameters
# process the form parameters
# process the body parameter
# set the HTTP header `Accept`
if 'Accept' not in _header_params:
_header_params['Accept'] = self.api_client.select_header_accept(
[
'application/json'
]
)
# authentication setting
_auth_settings: List[str] = [
]
return self.api_client.param_serialize(
method='GET',
resource_path='/api/v1/agents/{agent_id}/documents/{document_id}',
path_params=_path_params,
query_params=_query_params,
header_params=_header_params,
body=_body_params,
post_params=_form_params,
files=_files,
auth_settings=_auth_settings,
collection_formats=_collection_formats,
_host=_host,
_request_auth=_request_auth
)
@validate_call
async def list_documents(
self,
agent_id: StrictStr,
q: Optional[StrictStr] = None,
limit: Optional[StrictInt] = None,
offset: Optional[StrictInt] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ListDocumentsResponse:
"""List documents
List documents with pagination and optional search. Documents are the source content from which memory units are extracted.
:param agent_id: (required)
:type agent_id: str
:param q:
:type q: str
:param limit:
:type limit: int
:param offset:
:type offset: int
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._list_documents_serialize(
agent_id=agent_id,
q=q,
limit=limit,
offset=offset,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "ListDocumentsResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
).data
@validate_call
async def list_documents_with_http_info(
self,
agent_id: StrictStr,
q: Optional[StrictStr] = None,
limit: Optional[StrictInt] = None,
offset: Optional[StrictInt] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[ListDocumentsResponse]:
"""List documents
List documents with pagination and optional search. Documents are the source content from which memory units are extracted.
:param agent_id: (required)
:type agent_id: str
:param q:
:type q: str
:param limit:
:type limit: int
:param offset:
:type offset: int
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._list_documents_serialize(
agent_id=agent_id,
q=q,
limit=limit,
offset=offset,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "ListDocumentsResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
)
@validate_call
async def list_documents_without_preload_content(
self,
agent_id: StrictStr,
q: Optional[StrictStr] = None,
limit: Optional[StrictInt] = None,
offset: Optional[StrictInt] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> RESTResponseType:
"""List documents
List documents with pagination and optional search. Documents are the source content from which memory units are extracted.
:param agent_id: (required)
:type agent_id: str
:param q:
:type q: str
:param limit:
:type limit: int
:param offset:
:type offset: int
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._list_documents_serialize(
agent_id=agent_id,
q=q,
limit=limit,
offset=offset,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "ListDocumentsResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
return response_data.response
def _list_documents_serialize(
self,
agent_id,
q,
limit,
offset,
_request_auth,
_content_type,
_headers,
_host_index,
) -> RequestSerialized:
_host = None
_collection_formats: Dict[str, str] = {
}
_path_params: Dict[str, str] = {}
_query_params: List[Tuple[str, str]] = []
_header_params: Dict[str, Optional[str]] = _headers or {}
_form_params: List[Tuple[str, str]] = []
_files: Dict[
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
] = {}
_body_params: Optional[bytes] = None
# process the path parameters
if agent_id is not None:
_path_params['agent_id'] = agent_id
# process the query parameters
if q is not None:
_query_params.append(('q', q))
if limit is not None:
_query_params.append(('limit', limit))
if offset is not None:
_query_params.append(('offset', offset))
# process the header parameters
# process the form parameters
# process the body parameter
# set the HTTP header `Accept`
if 'Accept' not in _header_params:
_header_params['Accept'] = self.api_client.select_header_accept(
[
'application/json'
]
)
# authentication setting
_auth_settings: List[str] = [
]
return self.api_client.param_serialize(
method='GET',
resource_path='/api/v1/agents/{agent_id}/documents',
path_params=_path_params,
query_params=_query_params,
header_params=_header_params,
body=_body_params,
post_params=_form_params,
files=_files,
auth_settings=_auth_settings,
collection_formats=_collection_formats,
_host=_host,
_request_auth=_request_auth
)

View file

@ -1,329 +0,0 @@
# coding: utf-8
"""
Agent Memory API
A temporal-semantic memory system for AI agents that stores, retrieves, and reasons over memories. ## Features * **Batch Memory Storage**: Store multiple memories efficiently with automatic fact extraction * **Semantic Search**: Find relevant memories using natural language queries * **Fact Type Filtering**: Search across world facts, agent actions, and opinions separately * **Think Endpoint**: Generate contextual answers based on agent identity and memories * **Graph Visualization**: Interactive memory graph visualization * **Document Tracking**: Track and manage memory documents with upsert support ## Architecture The system uses: - **Temporal Links**: Connect memories that are close in time - **Semantic Links**: Connect semantically similar memories - **Entity Links**: Connect memories that mention the same entities - **Spreading Activation**: Intelligent traversal for memory retrieval
The version of the OpenAPI document: 1.0.0
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
import warnings
from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt
from typing import Any, Dict, List, Optional, Tuple, Union
from typing_extensions import Annotated
from pydantic import StrictStr
from hindsight_client_api.models.think_request import ThinkRequest
from hindsight_client_api.models.think_response import ThinkResponse
from hindsight_client_api.api_client import ApiClient, RequestSerialized
from hindsight_client_api.api_response import ApiResponse
from hindsight_client_api.rest import RESTResponseType
class ReasoningApi:
"""NOTE: This class is auto generated by OpenAPI Generator
Ref: https://openapi-generator.tech
Do not edit the class manually.
"""
def __init__(self, api_client=None) -> None:
if api_client is None:
api_client = ApiClient.get_default()
self.api_client = api_client
@validate_call
async def think(
self,
agent_id: StrictStr,
think_request: ThinkRequest,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ThinkResponse:
"""Think and generate answer
Think and formulate an answer using agent identity, world facts, and opinions. This endpoint: 1. Retrieves agent facts (agent's identity) 2. Retrieves world facts relevant to the query 3. Retrieves existing opinions (agent's perspectives) 4. Uses LLM to formulate a contextual answer 5. Extracts and stores any new opinions formed 6. Returns plain text answer, the facts used, and new opinions
:param agent_id: (required)
:type agent_id: str
:param think_request: (required)
:type think_request: ThinkRequest
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._think_serialize(
agent_id=agent_id,
think_request=think_request,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "ThinkResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
).data
@validate_call
async def think_with_http_info(
self,
agent_id: StrictStr,
think_request: ThinkRequest,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[ThinkResponse]:
"""Think and generate answer
Think and formulate an answer using agent identity, world facts, and opinions. This endpoint: 1. Retrieves agent facts (agent's identity) 2. Retrieves world facts relevant to the query 3. Retrieves existing opinions (agent's perspectives) 4. Uses LLM to formulate a contextual answer 5. Extracts and stores any new opinions formed 6. Returns plain text answer, the facts used, and new opinions
:param agent_id: (required)
:type agent_id: str
:param think_request: (required)
:type think_request: ThinkRequest
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._think_serialize(
agent_id=agent_id,
think_request=think_request,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "ThinkResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
)
@validate_call
async def think_without_preload_content(
self,
agent_id: StrictStr,
think_request: ThinkRequest,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> RESTResponseType:
"""Think and generate answer
Think and formulate an answer using agent identity, world facts, and opinions. This endpoint: 1. Retrieves agent facts (agent's identity) 2. Retrieves world facts relevant to the query 3. Retrieves existing opinions (agent's perspectives) 4. Uses LLM to formulate a contextual answer 5. Extracts and stores any new opinions formed 6. Returns plain text answer, the facts used, and new opinions
:param agent_id: (required)
:type agent_id: str
:param think_request: (required)
:type think_request: ThinkRequest
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._think_serialize(
agent_id=agent_id,
think_request=think_request,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "ThinkResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
return response_data.response
def _think_serialize(
self,
agent_id,
think_request,
_request_auth,
_content_type,
_headers,
_host_index,
) -> RequestSerialized:
_host = None
_collection_formats: Dict[str, str] = {
}
_path_params: Dict[str, str] = {}
_query_params: List[Tuple[str, str]] = []
_header_params: Dict[str, Optional[str]] = _headers or {}
_form_params: List[Tuple[str, str]] = []
_files: Dict[
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
] = {}
_body_params: Optional[bytes] = None
# process the path parameters
if agent_id is not None:
_path_params['agent_id'] = agent_id
# process the query parameters
# process the header parameters
# process the form parameters
# process the body parameter
if think_request is not None:
_body_params = think_request
# set the HTTP header `Accept`
if 'Accept' not in _header_params:
_header_params['Accept'] = self.api_client.select_header_accept(
[
'application/json'
]
)
# set the HTTP header `Content-Type`
if _content_type:
_header_params['Content-Type'] = _content_type
else:
_default_content_type = (
self.api_client.select_header_content_type(
[
'application/json'
]
)
)
if _default_content_type is not None:
_header_params['Content-Type'] = _default_content_type
# authentication setting
_auth_settings: List[str] = [
]
return self.api_client.param_serialize(
method='POST',
resource_path='/api/v1/agents/{agent_id}/think',
path_params=_path_params,
query_params=_query_params,
header_params=_header_params,
body=_body_params,
post_params=_form_params,
files=_files,
auth_settings=_auth_settings,
collection_formats=_collection_formats,
_host=_host,
_request_auth=_request_auth
)

View file

@ -1,318 +0,0 @@
# coding: utf-8
"""
Agent Memory API
A temporal-semantic memory system for AI agents that stores, retrieves, and reasons over memories. ## Features * **Batch Memory Storage**: Store multiple memories efficiently with automatic fact extraction * **Semantic Search**: Find relevant memories using natural language queries * **Fact Type Filtering**: Search across world facts, agent actions, and opinions separately * **Think Endpoint**: Generate contextual answers based on agent identity and memories * **Graph Visualization**: Interactive memory graph visualization * **Document Tracking**: Track and manage memory documents with upsert support ## Architecture The system uses: - **Temporal Links**: Connect memories that are close in time - **Semantic Links**: Connect semantically similar memories - **Entity Links**: Connect memories that mention the same entities - **Spreading Activation**: Intelligent traversal for memory retrieval
The version of the OpenAPI document: 1.0.0
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
import warnings
from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt
from typing import Any, Dict, List, Optional, Tuple, Union
from typing_extensions import Annotated
from pydantic import StrictStr
from typing import Optional
from hindsight_client_api.models.graph_data_response import GraphDataResponse
from hindsight_client_api.api_client import ApiClient, RequestSerialized
from hindsight_client_api.api_response import ApiResponse
from hindsight_client_api.rest import RESTResponseType
class VisualizationApi:
"""NOTE: This class is auto generated by OpenAPI Generator
Ref: https://openapi-generator.tech
Do not edit the class manually.
"""
def __init__(self, api_client=None) -> None:
if api_client is None:
api_client = ApiClient.get_default()
self.api_client = api_client
@validate_call
async def get_graph(
self,
agent_id: StrictStr,
fact_type: Optional[StrictStr] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> GraphDataResponse:
"""Get memory graph data
Retrieve graph data for visualization, optionally filtered by fact_type (world/agent/opinion). Limited to 1000 most recent items.
:param agent_id: (required)
:type agent_id: str
:param fact_type:
:type fact_type: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._get_graph_serialize(
agent_id=agent_id,
fact_type=fact_type,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "GraphDataResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
).data
@validate_call
async def get_graph_with_http_info(
self,
agent_id: StrictStr,
fact_type: Optional[StrictStr] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[GraphDataResponse]:
"""Get memory graph data
Retrieve graph data for visualization, optionally filtered by fact_type (world/agent/opinion). Limited to 1000 most recent items.
:param agent_id: (required)
:type agent_id: str
:param fact_type:
:type fact_type: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._get_graph_serialize(
agent_id=agent_id,
fact_type=fact_type,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "GraphDataResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
)
@validate_call
async def get_graph_without_preload_content(
self,
agent_id: StrictStr,
fact_type: Optional[StrictStr] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> RESTResponseType:
"""Get memory graph data
Retrieve graph data for visualization, optionally filtered by fact_type (world/agent/opinion). Limited to 1000 most recent items.
:param agent_id: (required)
:type agent_id: str
:param fact_type:
:type fact_type: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._get_graph_serialize(
agent_id=agent_id,
fact_type=fact_type,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "GraphDataResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
return response_data.response
def _get_graph_serialize(
self,
agent_id,
fact_type,
_request_auth,
_content_type,
_headers,
_host_index,
) -> RequestSerialized:
_host = None
_collection_formats: Dict[str, str] = {
}
_path_params: Dict[str, str] = {}
_query_params: List[Tuple[str, str]] = []
_header_params: Dict[str, Optional[str]] = _headers or {}
_form_params: List[Tuple[str, str]] = []
_files: Dict[
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
] = {}
_body_params: Optional[bytes] = None
# process the path parameters
if agent_id is not None:
_path_params['agent_id'] = agent_id
# process the query parameters
if fact_type is not None:
_query_params.append(('fact_type', fact_type))
# process the header parameters
# process the form parameters
# process the body parameter
# set the HTTP header `Accept`
if 'Accept' not in _header_params:
_header_params['Accept'] = self.api_client.select_header_accept(
[
'application/json'
]
)
# authentication setting
_auth_settings: List[str] = [
]
return self.api_client.param_serialize(
method='GET',
resource_path='/api/v1/agents/{agent_id}/graph',
path_params=_path_params,
query_params=_query_params,
header_params=_header_params,
body=_body_params,
post_params=_form_params,
files=_files,
auth_settings=_auth_settings,
collection_formats=_collection_formats,
_host=_host,
_request_auth=_request_auth
)

View file

@ -1,9 +1,9 @@
# coding: utf-8
"""
Agent Memory API
Hindsight HTTP API
A temporal-semantic memory system for AI agents that stores, retrieves, and reasons over memories. ## Features * **Batch Memory Storage**: Store multiple memories efficiently with automatic fact extraction * **Semantic Search**: Find relevant memories using natural language queries * **Fact Type Filtering**: Search across world facts, agent actions, and opinions separately * **Think Endpoint**: Generate contextual answers based on agent identity and memories * **Graph Visualization**: Interactive memory graph visualization * **Document Tracking**: Track and manage memory documents with upsert support ## Architecture The system uses: - **Temporal Links**: Connect memories that are close in time - **Semantic Links**: Connect semantically similar memories - **Entity Links**: Connect memories that mention the same entities - **Spreading Activation**: Intelligent traversal for memory retrieval
HTTP API for Hindsight
The version of the OpenAPI document: 1.0.0
Generated by OpenAPI Generator (https://openapi-generator.tech)

View file

@ -1,9 +1,9 @@
# coding: utf-8
"""
Agent Memory API
Hindsight HTTP API
A temporal-semantic memory system for AI agents that stores, retrieves, and reasons over memories. ## Features * **Batch Memory Storage**: Store multiple memories efficiently with automatic fact extraction * **Semantic Search**: Find relevant memories using natural language queries * **Fact Type Filtering**: Search across world facts, agent actions, and opinions separately * **Think Endpoint**: Generate contextual answers based on agent identity and memories * **Graph Visualization**: Interactive memory graph visualization * **Document Tracking**: Track and manage memory documents with upsert support ## Architecture The system uses: - **Temporal Links**: Connect memories that are close in time - **Semantic Links**: Connect semantically similar memories - **Entity Links**: Connect memories that mention the same entities - **Spreading Activation**: Intelligent traversal for memory retrieval
HTTP API for Hindsight
The version of the OpenAPI document: 1.0.0
Generated by OpenAPI Generator (https://openapi-generator.tech)

View file

@ -1,30 +0,0 @@
# AgentListResponse
Response model for listing all agents.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**agents** | [**List[AgentListItem]**](AgentListItem.md) | |
## Example
```python
from hindsight_client_api.models.agent_list_response import AgentListResponse
# TODO update the JSON string below
json = "{}"
# create an instance of AgentListResponse from a JSON string
agent_list_response_instance = AgentListResponse.from_json(json)
# print the JSON string representation of the object
print(AgentListResponse.to_json())
# convert the object into a dict
agent_list_response_dict = agent_list_response_instance.to_dict()
# create an instance of AgentListResponse from a dict
agent_list_response_from_dict = AgentListResponse.from_dict(agent_list_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,503 +0,0 @@
# hindsight_client_api.AgentManagementApi
All URIs are relative to *http://localhost*
Method | HTTP request | Description
------------- | ------------- | -------------
[**add_agent_background**](AgentManagementApi.md#add_agent_background) | **POST** /api/v1/agents/{agent_id}/background | Add/merge agent background
[**clear_agent_memories**](AgentManagementApi.md#clear_agent_memories) | **DELETE** /api/v1/agents/{agent_id}/memories | Clear agent memories
[**create_or_update_agent**](AgentManagementApi.md#create_or_update_agent) | **PUT** /api/v1/agents/{agent_id} | Create or update agent
[**get_agent_profile**](AgentManagementApi.md#get_agent_profile) | **GET** /api/v1/agents/{agent_id}/profile | Get agent profile
[**get_agent_stats**](AgentManagementApi.md#get_agent_stats) | **GET** /api/v1/agents/{agent_id}/stats | Get memory statistics for an agent
[**list_agents**](AgentManagementApi.md#list_agents) | **GET** /api/v1/agents | List all agents
[**update_agent_personality**](AgentManagementApi.md#update_agent_personality) | **PUT** /api/v1/agents/{agent_id}/profile | Update agent personality
# **add_agent_background**
> BackgroundResponse add_agent_background(agent_id, add_background_request)
Add/merge agent background
Add new background information or merge with existing. LLM intelligently resolves conflicts, normalizes to first person, and optionally infers personality traits.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.add_background_request import AddBackgroundRequest
from hindsight_client_api.models.background_response import BackgroundResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.AgentManagementApi(api_client)
agent_id = 'agent_id_example' # str |
add_background_request = hindsight_client_api.AddBackgroundRequest() # AddBackgroundRequest |
try:
# Add/merge agent background
api_response = await api_instance.add_agent_background(agent_id, add_background_request)
print("The response of AgentManagementApi->add_agent_background:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling AgentManagementApi->add_agent_background: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**agent_id** | **str**| |
**add_background_request** | [**AddBackgroundRequest**](AddBackgroundRequest.md)| |
### Return type
[**BackgroundResponse**](BackgroundResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **clear_agent_memories**
> DeleteResponse clear_agent_memories(agent_id, fact_type=fact_type)
Clear agent memories
Delete memory units for an agent. Optionally filter by fact_type (world, agent, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The agent profile (personality and background) will be preserved.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.delete_response import DeleteResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.AgentManagementApi(api_client)
agent_id = 'agent_id_example' # str |
fact_type = 'fact_type_example' # str | Optional fact type filter (world, agent, opinion) (optional)
try:
# Clear agent memories
api_response = await api_instance.clear_agent_memories(agent_id, fact_type=fact_type)
print("The response of AgentManagementApi->clear_agent_memories:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling AgentManagementApi->clear_agent_memories: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**agent_id** | **str**| |
**fact_type** | **str**| Optional fact type filter (world, agent, opinion) | [optional]
### Return type
[**DeleteResponse**](DeleteResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **create_or_update_agent**
> AgentProfileResponse create_or_update_agent(agent_id, create_agent_request)
Create or update agent
Create a new agent or update existing agent with personality and background. Auto-fills missing fields with defaults.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.agent_profile_response import AgentProfileResponse
from hindsight_client_api.models.create_agent_request import CreateAgentRequest
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.AgentManagementApi(api_client)
agent_id = 'agent_id_example' # str |
create_agent_request = hindsight_client_api.CreateAgentRequest() # CreateAgentRequest |
try:
# Create or update agent
api_response = await api_instance.create_or_update_agent(agent_id, create_agent_request)
print("The response of AgentManagementApi->create_or_update_agent:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling AgentManagementApi->create_or_update_agent: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**agent_id** | **str**| |
**create_agent_request** | [**CreateAgentRequest**](CreateAgentRequest.md)| |
### Return type
[**AgentProfileResponse**](AgentProfileResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **get_agent_profile**
> AgentProfileResponse get_agent_profile(agent_id)
Get agent profile
Get personality traits and background for an agent. Auto-creates agent with defaults if not exists.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.agent_profile_response import AgentProfileResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.AgentManagementApi(api_client)
agent_id = 'agent_id_example' # str |
try:
# Get agent profile
api_response = await api_instance.get_agent_profile(agent_id)
print("The response of AgentManagementApi->get_agent_profile:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling AgentManagementApi->get_agent_profile: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**agent_id** | **str**| |
### Return type
[**AgentProfileResponse**](AgentProfileResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **get_agent_stats**
> object get_agent_stats(agent_id)
Get memory statistics for an agent
Get statistics about nodes and links for a specific agent
### Example
```python
import hindsight_client_api
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.AgentManagementApi(api_client)
agent_id = 'agent_id_example' # str |
try:
# Get memory statistics for an agent
api_response = await api_instance.get_agent_stats(agent_id)
print("The response of AgentManagementApi->get_agent_stats:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling AgentManagementApi->get_agent_stats: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**agent_id** | **str**| |
### Return type
**object**
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **list_agents**
> AgentListResponse list_agents()
List all agents
Get a list of all agents with their profiles
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.agent_list_response import AgentListResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.AgentManagementApi(api_client)
try:
# List all agents
api_response = await api_instance.list_agents()
print("The response of AgentManagementApi->list_agents:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling AgentManagementApi->list_agents: %s\n" % e)
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**AgentListResponse**](AgentListResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **update_agent_personality**
> AgentProfileResponse update_agent_personality(agent_id, update_personality_request)
Update agent personality
Update agent's Big Five personality traits and bias strength
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.agent_profile_response import AgentProfileResponse
from hindsight_client_api.models.update_personality_request import UpdatePersonalityRequest
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.AgentManagementApi(api_client)
agent_id = 'agent_id_example' # str |
update_personality_request = hindsight_client_api.UpdatePersonalityRequest() # UpdatePersonalityRequest |
try:
# Update agent personality
api_response = await api_instance.update_agent_personality(agent_id, update_personality_request)
print("The response of AgentManagementApi->update_agent_personality:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling AgentManagementApi->update_agent_personality: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**agent_id** | **str**| |
**update_personality_request** | [**UpdatePersonalityRequest**](UpdatePersonalityRequest.md)| |
### Return type
[**AgentProfileResponse**](AgentProfileResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

View file

@ -1,33 +0,0 @@
# AgentProfileResponse
Response model for agent profile.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**agent_id** | **str** | |
**name** | **str** | |
**personality** | [**PersonalityTraits**](PersonalityTraits.md) | |
**background** | **str** | |
## Example
```python
from hindsight_client_api.models.agent_profile_response import AgentProfileResponse
# TODO update the JSON string below
json = "{}"
# create an instance of AgentProfileResponse from a JSON string
agent_profile_response_instance = AgentProfileResponse.from_json(json)
# print the JSON string representation of the object
print(AgentProfileResponse.to_json())
# convert the object into a dict
agent_profile_response_dict = agent_profile_response_instance.to_dict()
# create an instance of AgentProfileResponse from a dict
agent_profile_response_from_dict = AgentProfileResponse.from_dict(agent_profile_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,12 +1,12 @@
# AgentListItem
# BankListItem
Agent list item with profile summary.
Bank list item with profile summary.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**agent_id** | **str** | |
**bank_id** | **str** | |
**name** | **str** | |
**personality** | [**PersonalityTraits**](PersonalityTraits.md) | |
**background** | **str** | |
@ -16,19 +16,19 @@ Name | Type | Description | Notes
## Example
```python
from hindsight_client_api.models.agent_list_item import AgentListItem
from hindsight_client_api.models.bank_list_item import BankListItem
# TODO update the JSON string below
json = "{}"
# create an instance of AgentListItem from a JSON string
agent_list_item_instance = AgentListItem.from_json(json)
# create an instance of BankListItem from a JSON string
bank_list_item_instance = BankListItem.from_json(json)
# print the JSON string representation of the object
print(AgentListItem.to_json())
print(BankListItem.to_json())
# convert the object into a dict
agent_list_item_dict = agent_list_item_instance.to_dict()
# create an instance of AgentListItem from a dict
agent_list_item_from_dict = AgentListItem.from_dict(agent_list_item_dict)
bank_list_item_dict = bank_list_item_instance.to_dict()
# create an instance of BankListItem from a dict
bank_list_item_from_dict = BankListItem.from_dict(bank_list_item_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,30 @@
# BankListResponse
Response model for listing all banks.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**banks** | [**List[BankListItem]**](BankListItem.md) | |
## Example
```python
from hindsight_client_api.models.bank_list_response import BankListResponse
# TODO update the JSON string below
json = "{}"
# create an instance of BankListResponse from a JSON string
bank_list_response_instance = BankListResponse.from_json(json)
# print the JSON string representation of the object
print(BankListResponse.to_json())
# convert the object into a dict
bank_list_response_dict = bank_list_response_instance.to_dict()
# create an instance of BankListResponse from a dict
bank_list_response_from_dict = BankListResponse.from_dict(bank_list_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,33 @@
# BankProfileResponse
Response model for bank profile.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**bank_id** | **str** | |
**name** | **str** | |
**personality** | [**PersonalityTraits**](PersonalityTraits.md) | |
**background** | **str** | |
## Example
```python
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
# TODO update the JSON string below
json = "{}"
# create an instance of BankProfileResponse from a JSON string
bank_profile_response_instance = BankProfileResponse.from_json(json)
# print the JSON string representation of the object
print(BankProfileResponse.to_json())
# convert the object into a dict
bank_profile_response_dict = bank_profile_response_instance.to_dict()
# create an instance of BankProfileResponse from a dict
bank_profile_response_from_dict = BankProfileResponse.from_dict(bank_profile_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,35 +0,0 @@
# BatchPutAsyncResponse
Response model for async batch put endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**success** | **bool** | |
**message** | **str** | |
**agent_id** | **str** | |
**document_id** | **str** | | [optional]
**items_count** | **int** | |
**queued** | **bool** | |
## Example
```python
from hindsight_client_api.models.batch_put_async_response import BatchPutAsyncResponse
# TODO update the JSON string below
json = "{}"
# create an instance of BatchPutAsyncResponse from a JSON string
batch_put_async_response_instance = BatchPutAsyncResponse.from_json(json)
# print the JSON string representation of the object
print(BatchPutAsyncResponse.to_json())
# convert the object into a dict
batch_put_async_response_dict = batch_put_async_response_instance.to_dict()
# create an instance of BatchPutAsyncResponse from a dict
batch_put_async_response_from_dict = BatchPutAsyncResponse.from_dict(batch_put_async_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,31 +0,0 @@
# BatchPutRequest
Request model for batch put endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**items** | [**List[MemoryItem]**](MemoryItem.md) | |
**document_id** | **str** | | [optional]
## Example
```python
from hindsight_client_api.models.batch_put_request import BatchPutRequest
# TODO update the JSON string below
json = "{}"
# create an instance of BatchPutRequest from a JSON string
batch_put_request_instance = BatchPutRequest.from_json(json)
# print the JSON string representation of the object
print(BatchPutRequest.to_json())
# convert the object into a dict
batch_put_request_dict = batch_put_request_instance.to_dict()
# create an instance of BatchPutRequest from a dict
batch_put_request_from_dict = BatchPutRequest.from_dict(batch_put_request_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,34 +0,0 @@
# BatchPutResponse
Response model for batch put endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**success** | **bool** | |
**message** | **str** | |
**agent_id** | **str** | |
**document_id** | **str** | | [optional]
**items_count** | **int** | |
## Example
```python
from hindsight_client_api.models.batch_put_response import BatchPutResponse
# TODO update the JSON string below
json = "{}"
# create an instance of BatchPutResponse from a JSON string
batch_put_response_instance = BatchPutResponse.from_json(json)
# print the JSON string representation of the object
print(BatchPutResponse.to_json())
# convert the object into a dict
batch_put_response_dict = batch_put_response_instance.to_dict()
# create an instance of BatchPutResponse from a dict
batch_put_response_from_dict = BatchPutResponse.from_dict(batch_put_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,15 @@
# Budget
Budget levels for recall/reflect operations.
## Enum
* `LOW` (value: `'low'`)
* `MID` (value: `'mid'`)
* `HIGH` (value: `'high'`)
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,6 +1,6 @@
# CreateAgentRequest
# CreateBankRequest
Request model for creating/updating an agent.
Request model for creating/updating a bank.
## Properties
@ -13,19 +13,19 @@ Name | Type | Description | Notes
## Example
```python
from hindsight_client_api.models.create_agent_request import CreateAgentRequest
from hindsight_client_api.models.create_bank_request import CreateBankRequest
# TODO update the JSON string below
json = "{}"
# create an instance of CreateAgentRequest from a JSON string
create_agent_request_instance = CreateAgentRequest.from_json(json)
# create an instance of CreateBankRequest from a JSON string
create_bank_request_instance = CreateBankRequest.from_json(json)
# print the JSON string representation of the object
print(CreateAgentRequest.to_json())
print(CreateBankRequest.to_json())
# convert the object into a dict
create_agent_request_dict = create_agent_request_instance.to_dict()
# create an instance of CreateAgentRequest from a dict
create_agent_request_from_dict = CreateAgentRequest.from_dict(create_agent_request_dict)
create_bank_request_dict = create_bank_request_instance.to_dict()
# create an instance of CreateBankRequest from a dict
create_bank_request_from_dict = CreateBankRequest.from_dict(create_bank_request_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,6 @@ Response model for delete operations.
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**success** | **bool** | |
**message** | **str** | |
## Example

View file

@ -1,234 +0,0 @@
# hindsight_client_api.DocumentsApi
All URIs are relative to *http://localhost*
Method | HTTP request | Description
------------- | ------------- | -------------
[**delete_document**](DocumentsApi.md#delete_document) | **DELETE** /api/v1/agents/{agent_id}/documents/{document_id} | Delete a document
[**get_document**](DocumentsApi.md#get_document) | **GET** /api/v1/agents/{agent_id}/documents/{document_id} | Get document details
[**list_documents**](DocumentsApi.md#list_documents) | **GET** /api/v1/agents/{agent_id}/documents | List documents
# **delete_document**
> object delete_document(agent_id, document_id)
Delete a document
Delete a document and all its associated memory units and links.
This will cascade delete:
- The document itself
- All memory units extracted from this document
- All links (temporal, semantic, entity) associated with those memory units
This operation cannot be undone.
### Example
```python
import hindsight_client_api
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.DocumentsApi(api_client)
agent_id = 'agent_id_example' # str |
document_id = 'document_id_example' # str |
try:
# Delete a document
api_response = await api_instance.delete_document(agent_id, document_id)
print("The response of DocumentsApi->delete_document:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling DocumentsApi->delete_document: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**agent_id** | **str**| |
**document_id** | **str**| |
### Return type
**object**
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **get_document**
> DocumentResponse get_document(agent_id, document_id)
Get document details
Get a specific document including its original text
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.document_response import DocumentResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.DocumentsApi(api_client)
agent_id = 'agent_id_example' # str |
document_id = 'document_id_example' # str |
try:
# Get document details
api_response = await api_instance.get_document(agent_id, document_id)
print("The response of DocumentsApi->get_document:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling DocumentsApi->get_document: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**agent_id** | **str**| |
**document_id** | **str**| |
### Return type
[**DocumentResponse**](DocumentResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **list_documents**
> ListDocumentsResponse list_documents(agent_id, q=q, limit=limit, offset=offset)
List documents
List documents with pagination and optional search. Documents are the source content from which memory units are extracted.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.list_documents_response import ListDocumentsResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.DocumentsApi(api_client)
agent_id = 'agent_id_example' # str |
q = 'q_example' # str | (optional)
limit = 100 # int | (optional) (default to 100)
offset = 0 # int | (optional) (default to 0)
try:
# List documents
api_response = await api_instance.list_documents(agent_id, q=q, limit=limit, offset=offset)
print("The response of DocumentsApi->list_documents:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling DocumentsApi->list_documents: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**agent_id** | **str**| |
**q** | **str**| | [optional]
**limit** | **int**| | [optional] [default to 100]
**offset** | **int**| | [optional] [default to 0]
### Return type
[**ListDocumentsResponse**](ListDocumentsResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

View file

@ -0,0 +1,36 @@
# EntityDetailResponse
Response model for entity detail endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **str** | |
**canonical_name** | **str** | |
**mention_count** | **int** | |
**first_seen** | **str** | | [optional]
**last_seen** | **str** | | [optional]
**metadata** | **Dict[str, object]** | | [optional]
**observations** | [**List[EntityObservationResponse]**](EntityObservationResponse.md) | |
## Example
```python
from hindsight_client_api.models.entity_detail_response import EntityDetailResponse
# TODO update the JSON string below
json = "{}"
# create an instance of EntityDetailResponse from a JSON string
entity_detail_response_instance = EntityDetailResponse.from_json(json)
# print the JSON string representation of the object
print(EntityDetailResponse.to_json())
# convert the object into a dict
entity_detail_response_dict = entity_detail_response_instance.to_dict()
# create an instance of EntityDetailResponse from a dict
entity_detail_response_from_dict = EntityDetailResponse.from_dict(entity_detail_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,30 @@
# EntityIncludeOptions
Options for including entity observations in recall results.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**max_tokens** | **int** | Maximum tokens for entity observations | [optional] [default to 500]
## Example
```python
from hindsight_client_api.models.entity_include_options import EntityIncludeOptions
# TODO update the JSON string below
json = "{}"
# create an instance of EntityIncludeOptions from a JSON string
entity_include_options_instance = EntityIncludeOptions.from_json(json)
# print the JSON string representation of the object
print(EntityIncludeOptions.to_json())
# convert the object into a dict
entity_include_options_dict = entity_include_options_instance.to_dict()
# create an instance of EntityIncludeOptions from a dict
entity_include_options_from_dict = EntityIncludeOptions.from_dict(entity_include_options_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,35 @@
# EntityListItem
Entity list item with summary.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **str** | |
**canonical_name** | **str** | |
**mention_count** | **int** | |
**first_seen** | **str** | | [optional]
**last_seen** | **str** | | [optional]
**metadata** | **Dict[str, object]** | | [optional]
## Example
```python
from hindsight_client_api.models.entity_list_item import EntityListItem
# TODO update the JSON string below
json = "{}"
# create an instance of EntityListItem from a JSON string
entity_list_item_instance = EntityListItem.from_json(json)
# print the JSON string representation of the object
print(EntityListItem.to_json())
# convert the object into a dict
entity_list_item_dict = entity_list_item_instance.to_dict()
# create an instance of EntityListItem from a dict
entity_list_item_from_dict = EntityListItem.from_dict(entity_list_item_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,30 @@
# EntityListResponse
Response model for entity list endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**entities** | [**List[EntityListItem]**](EntityListItem.md) | |
## Example
```python
from hindsight_client_api.models.entity_list_response import EntityListResponse
# TODO update the JSON string below
json = "{}"
# create an instance of EntityListResponse from a JSON string
entity_list_response_instance = EntityListResponse.from_json(json)
# print the JSON string representation of the object
print(EntityListResponse.to_json())
# convert the object into a dict
entity_list_response_dict = entity_list_response_instance.to_dict()
# create an instance of EntityListResponse from a dict
entity_list_response_from_dict = EntityListResponse.from_dict(entity_list_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,31 @@
# EntityObservationResponse
An observation about an entity.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**text** | **str** | |
**mentioned_at** | **str** | | [optional]
## Example
```python
from hindsight_client_api.models.entity_observation_response import EntityObservationResponse
# TODO update the JSON string below
json = "{}"
# create an instance of EntityObservationResponse from a JSON string
entity_observation_response_instance = EntityObservationResponse.from_json(json)
# print the JSON string representation of the object
print(EntityObservationResponse.to_json())
# convert the object into a dict
entity_observation_response_dict = entity_observation_response_instance.to_dict()
# create an instance of EntityObservationResponse from a dict
entity_observation_response_from_dict = EntityObservationResponse.from_dict(entity_observation_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,32 @@
# EntityStateResponse
Current mental model of an entity.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**entity_id** | **str** | |
**canonical_name** | **str** | |
**observations** | [**List[EntityObservationResponse]**](EntityObservationResponse.md) | |
## Example
```python
from hindsight_client_api.models.entity_state_response import EntityStateResponse
# TODO update the JSON string below
json = "{}"
# create an instance of EntityStateResponse from a JSON string
entity_state_response_instance = EntityStateResponse.from_json(json)
# print the JSON string representation of the object
print(EntityStateResponse.to_json())
# convert the object into a dict
entity_state_response_dict = entity_state_response_instance.to_dict()
# create an instance of EntityStateResponse from a dict
entity_state_response_from_dict = EntityStateResponse.from_dict(entity_state_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,30 @@
# IncludeOptions
Options for including additional data in recall results.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**entities** | [**EntityIncludeOptions**](EntityIncludeOptions.md) | | [optional]
## Example
```python
from hindsight_client_api.models.include_options import IncludeOptions
# TODO update the JSON string below
json = "{}"
# create an instance of IncludeOptions from a JSON string
include_options_instance = IncludeOptions.from_json(json)
# print the JSON string representation of the object
print(IncludeOptions.to_json())
# convert the object into a dict
include_options_dict = include_options_instance.to_dict()
# create an instance of IncludeOptions from a dict
include_options_from_dict = IncludeOptions.from_dict(include_options_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -1,14 +1,15 @@
# MemoryItem
Single memory item for batch put.
Single memory item for retain.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**content** | **str** | |
**event_date** | **datetime** | | [optional]
**timestamp** | **datetime** | | [optional]
**context** | **str** | | [optional]
**metadata** | **Dict[str, str]** | | [optional]
## Example

Some files were not shown because too many files have changed in this diff Show more