hindsight-api 0.3.0__py3-none-any.whl → 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hindsight_api/__init__.py +1 -1
- hindsight_api/admin/cli.py +59 -0
- hindsight_api/alembic/versions/h3c4d5e6f7g8_mental_models_v4.py +112 -0
- hindsight_api/alembic/versions/i4d5e6f7g8h9_delete_opinions.py +41 -0
- hindsight_api/alembic/versions/j5e6f7g8h9i0_mental_model_versions.py +95 -0
- hindsight_api/alembic/versions/k6f7g8h9i0j1_add_directive_subtype.py +58 -0
- hindsight_api/alembic/versions/l7g8h9i0j1k2_add_worker_columns.py +109 -0
- hindsight_api/alembic/versions/m8h9i0j1k2l3_mental_model_id_to_text.py +41 -0
- hindsight_api/alembic/versions/n9i0j1k2l3m4_learnings_and_pinned_reflections.py +134 -0
- hindsight_api/alembic/versions/o0j1k2l3m4n5_migrate_mental_models_data.py +113 -0
- hindsight_api/alembic/versions/p1k2l3m4n5o6_new_knowledge_architecture.py +194 -0
- hindsight_api/alembic/versions/q2l3m4n5o6p7_fix_mental_model_fact_type.py +50 -0
- hindsight_api/alembic/versions/r3m4n5o6p7q8_add_reflect_response_to_reflections.py +47 -0
- hindsight_api/alembic/versions/s4n5o6p7q8r9_add_consolidated_at_to_memory_units.py +53 -0
- hindsight_api/alembic/versions/t5o6p7q8r9s0_rename_mental_models_to_observations.py +134 -0
- hindsight_api/alembic/versions/u6p7q8r9s0t1_mental_models_text_id.py +41 -0
- hindsight_api/alembic/versions/v7q8r9s0t1u2_add_max_tokens_to_mental_models.py +50 -0
- hindsight_api/api/http.py +1120 -93
- hindsight_api/api/mcp.py +11 -191
- hindsight_api/config.py +174 -46
- hindsight_api/engine/consolidation/__init__.py +5 -0
- hindsight_api/engine/consolidation/consolidator.py +926 -0
- hindsight_api/engine/consolidation/prompts.py +77 -0
- hindsight_api/engine/cross_encoder.py +153 -22
- hindsight_api/engine/directives/__init__.py +5 -0
- hindsight_api/engine/directives/models.py +37 -0
- hindsight_api/engine/embeddings.py +136 -13
- hindsight_api/engine/interface.py +32 -13
- hindsight_api/engine/llm_wrapper.py +505 -43
- hindsight_api/engine/memory_engine.py +2101 -1094
- hindsight_api/engine/mental_models/__init__.py +14 -0
- hindsight_api/engine/mental_models/models.py +53 -0
- hindsight_api/engine/reflect/__init__.py +18 -0
- hindsight_api/engine/reflect/agent.py +933 -0
- hindsight_api/engine/reflect/models.py +109 -0
- hindsight_api/engine/reflect/observations.py +186 -0
- hindsight_api/engine/reflect/prompts.py +483 -0
- hindsight_api/engine/reflect/tools.py +437 -0
- hindsight_api/engine/reflect/tools_schema.py +250 -0
- hindsight_api/engine/response_models.py +130 -4
- hindsight_api/engine/retain/bank_utils.py +79 -201
- hindsight_api/engine/retain/fact_extraction.py +81 -48
- hindsight_api/engine/retain/fact_storage.py +5 -8
- hindsight_api/engine/retain/link_utils.py +5 -8
- hindsight_api/engine/retain/orchestrator.py +1 -55
- hindsight_api/engine/retain/types.py +2 -2
- hindsight_api/engine/search/graph_retrieval.py +2 -2
- hindsight_api/engine/search/link_expansion_retrieval.py +164 -29
- hindsight_api/engine/search/mpfp_retrieval.py +1 -1
- hindsight_api/engine/search/retrieval.py +14 -14
- hindsight_api/engine/search/think_utils.py +41 -140
- hindsight_api/engine/search/trace.py +0 -1
- hindsight_api/engine/search/tracer.py +2 -5
- hindsight_api/engine/search/types.py +0 -3
- hindsight_api/engine/task_backend.py +112 -196
- hindsight_api/engine/utils.py +0 -151
- hindsight_api/extensions/__init__.py +10 -1
- hindsight_api/extensions/builtin/tenant.py +11 -4
- hindsight_api/extensions/operation_validator.py +81 -4
- hindsight_api/extensions/tenant.py +26 -0
- hindsight_api/main.py +28 -5
- hindsight_api/mcp_local.py +12 -53
- hindsight_api/mcp_tools.py +494 -0
- hindsight_api/models.py +0 -2
- hindsight_api/worker/__init__.py +11 -0
- hindsight_api/worker/main.py +296 -0
- hindsight_api/worker/poller.py +486 -0
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.1.dist-info}/METADATA +12 -6
- hindsight_api-0.4.1.dist-info/RECORD +112 -0
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.1.dist-info}/entry_points.txt +1 -0
- hindsight_api/engine/retain/observation_regeneration.py +0 -254
- hindsight_api/engine/search/observation_utils.py +0 -125
- hindsight_api/engine/search/scoring.py +0 -159
- hindsight_api-0.3.0.dist-info/RECORD +0 -82
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.1.dist-info}/WHEEL +0 -0
hindsight_api/api/http.py
CHANGED
|
@@ -10,7 +10,7 @@ import logging
|
|
|
10
10
|
import uuid
|
|
11
11
|
from contextlib import asynccontextmanager
|
|
12
12
|
from datetime import datetime
|
|
13
|
-
from typing import Any
|
|
13
|
+
from typing import Any, Literal
|
|
14
14
|
|
|
15
15
|
from fastapi import Depends, FastAPI, Header, HTTPException, Query
|
|
16
16
|
|
|
@@ -36,6 +36,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
36
36
|
from hindsight_api import MemoryEngine
|
|
37
37
|
from hindsight_api.engine.db_utils import acquire_with_retry
|
|
38
38
|
from hindsight_api.engine.memory_engine import Budget, fq_table
|
|
39
|
+
from hindsight_api.engine.reflect.observations import Observation
|
|
39
40
|
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES, TokenUsage
|
|
40
41
|
from hindsight_api.engine.search.tags import TagsMatch
|
|
41
42
|
from hindsight_api.extensions import HttpExtension, OperationValidationError, load_extension
|
|
@@ -90,7 +91,9 @@ class RecallRequest(BaseModel):
|
|
|
90
91
|
|
|
91
92
|
query: str
|
|
92
93
|
types: list[str] | None = Field(
|
|
93
|
-
default=None,
|
|
94
|
+
default=None,
|
|
95
|
+
description="List of fact types to recall: 'world', 'experience', 'observation'. Defaults to world and experience if not specified. "
|
|
96
|
+
"Note: 'opinion' is accepted but ignored (opinions are excluded from recall).",
|
|
94
97
|
)
|
|
95
98
|
budget: Budget = Budget.MID
|
|
96
99
|
max_tokens: int = 4096
|
|
@@ -427,6 +430,15 @@ class FactsIncludeOptions(BaseModel):
|
|
|
427
430
|
pass # No additional options needed, just enable/disable
|
|
428
431
|
|
|
429
432
|
|
|
433
|
+
class ToolCallsIncludeOptions(BaseModel):
|
|
434
|
+
"""Options for including tool calls in reflect results."""
|
|
435
|
+
|
|
436
|
+
output: bool = Field(
|
|
437
|
+
default=True,
|
|
438
|
+
description="Include tool outputs in the trace. Set to false to only include inputs (smaller payload).",
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
430
442
|
class ReflectIncludeOptions(BaseModel):
|
|
431
443
|
"""Options for including additional data in reflect results."""
|
|
432
444
|
|
|
@@ -434,6 +446,10 @@ class ReflectIncludeOptions(BaseModel):
|
|
|
434
446
|
default=None,
|
|
435
447
|
description="Include facts that the answer is based on. Set to {} to enable, null to disable (default: disabled).",
|
|
436
448
|
)
|
|
449
|
+
tool_calls: ToolCallsIncludeOptions | None = Field(
|
|
450
|
+
default=None,
|
|
451
|
+
description="Include tool calls trace. Set to {} for full trace (input+output), {output: false} for inputs only.",
|
|
452
|
+
)
|
|
437
453
|
|
|
438
454
|
|
|
439
455
|
class ReflectRequest(BaseModel):
|
|
@@ -444,7 +460,6 @@ class ReflectRequest(BaseModel):
|
|
|
444
460
|
"example": {
|
|
445
461
|
"query": "What do you think about artificial intelligence?",
|
|
446
462
|
"budget": "low",
|
|
447
|
-
"context": "This is for a research paper on AI ethics",
|
|
448
463
|
"max_tokens": 4096,
|
|
449
464
|
"include": {"facts": {}},
|
|
450
465
|
"response_schema": {
|
|
@@ -463,7 +478,13 @@ class ReflectRequest(BaseModel):
|
|
|
463
478
|
|
|
464
479
|
query: str
|
|
465
480
|
budget: Budget = Budget.LOW
|
|
466
|
-
context: str | None =
|
|
481
|
+
context: str | None = Field(
|
|
482
|
+
default=None,
|
|
483
|
+
description="DEPRECATED: Additional context is now concatenated with the query. "
|
|
484
|
+
"Pass context directly in the query field instead. "
|
|
485
|
+
"If provided, it will be appended to the query for backward compatibility.",
|
|
486
|
+
deprecated=True,
|
|
487
|
+
)
|
|
467
488
|
max_tokens: int = Field(default=4096, description="Maximum tokens for the response")
|
|
468
489
|
include: ReflectIncludeOptions = Field(
|
|
469
490
|
default_factory=ReflectIncludeOptions, description="Options for including additional data (disabled by default)"
|
|
@@ -514,6 +535,58 @@ class ReflectFact(BaseModel):
|
|
|
514
535
|
occurred_end: str | None = None
|
|
515
536
|
|
|
516
537
|
|
|
538
|
+
class ReflectDirective(BaseModel):
|
|
539
|
+
"""A directive applied during reflect."""
|
|
540
|
+
|
|
541
|
+
id: str = Field(description="Directive ID")
|
|
542
|
+
name: str = Field(description="Directive name")
|
|
543
|
+
content: str = Field(description="Directive content")
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class ReflectMentalModel(BaseModel):
|
|
547
|
+
"""A mental model used during reflect."""
|
|
548
|
+
|
|
549
|
+
id: str = Field(description="Mental model ID")
|
|
550
|
+
text: str = Field(description="Mental model content")
|
|
551
|
+
context: str | None = Field(default=None, description="Additional context")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
class ReflectToolCall(BaseModel):
|
|
555
|
+
"""A tool call made during reflect agent execution."""
|
|
556
|
+
|
|
557
|
+
tool: str = Field(description="Tool name: lookup, recall, learn, expand")
|
|
558
|
+
input: dict = Field(description="Tool input parameters")
|
|
559
|
+
output: dict | None = Field(
|
|
560
|
+
default=None, description="Tool output (only included when include.tool_calls.output is true)"
|
|
561
|
+
)
|
|
562
|
+
duration_ms: int = Field(description="Execution time in milliseconds")
|
|
563
|
+
iteration: int = Field(default=0, description="Iteration number (1-based) when this tool was called")
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
class ReflectLLMCall(BaseModel):
|
|
567
|
+
"""An LLM call made during reflect agent execution."""
|
|
568
|
+
|
|
569
|
+
scope: str = Field(description="Call scope: agent_1, agent_2, final, etc.")
|
|
570
|
+
duration_ms: int = Field(description="Execution time in milliseconds")
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
class ReflectBasedOn(BaseModel):
|
|
574
|
+
"""Evidence the response is based on: memories, mental models, and directives."""
|
|
575
|
+
|
|
576
|
+
memories: list[ReflectFact] = Field(default_factory=list, description="Memory facts used to generate the response")
|
|
577
|
+
mental_models: list[ReflectMentalModel] = Field(
|
|
578
|
+
default_factory=list, description="Mental models used during reflection"
|
|
579
|
+
)
|
|
580
|
+
directives: list[ReflectDirective] = Field(default_factory=list, description="Directives applied during reflection")
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
class ReflectTrace(BaseModel):
|
|
584
|
+
"""Execution trace of LLM and tool calls during reflection."""
|
|
585
|
+
|
|
586
|
+
tool_calls: list[ReflectToolCall] = Field(default_factory=list, description="Tool calls made during reflection")
|
|
587
|
+
llm_calls: list[ReflectLLMCall] = Field(default_factory=list, description="LLM calls made during reflection")
|
|
588
|
+
|
|
589
|
+
|
|
517
590
|
class ReflectResponse(BaseModel):
|
|
518
591
|
"""Response model for think endpoint."""
|
|
519
592
|
|
|
@@ -521,21 +594,38 @@ class ReflectResponse(BaseModel):
|
|
|
521
594
|
json_schema_extra={
|
|
522
595
|
"example": {
|
|
523
596
|
"text": "Based on my understanding, AI is a transformative technology...",
|
|
524
|
-
"based_on":
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
597
|
+
"based_on": {
|
|
598
|
+
"memories": [
|
|
599
|
+
{"id": "123", "text": "AI is used in healthcare", "type": "world"},
|
|
600
|
+
{"id": "456", "text": "I discussed AI applications last week", "type": "experience"},
|
|
601
|
+
],
|
|
602
|
+
},
|
|
528
603
|
"structured_output": {
|
|
529
604
|
"summary": "AI is transformative",
|
|
530
605
|
"key_points": ["Used in healthcare", "Discussed recently"],
|
|
531
606
|
},
|
|
532
607
|
"usage": {"input_tokens": 1500, "output_tokens": 500, "total_tokens": 2000},
|
|
608
|
+
"trace": {
|
|
609
|
+
"tool_calls": [{"tool": "recall", "input": {"query": "AI"}, "duration_ms": 150}],
|
|
610
|
+
"llm_calls": [{"scope": "agent_1", "duration_ms": 1200}],
|
|
611
|
+
"observations": [
|
|
612
|
+
{
|
|
613
|
+
"id": "obs-1",
|
|
614
|
+
"name": "AI Technology",
|
|
615
|
+
"type": "concept",
|
|
616
|
+
"subtype": "structural",
|
|
617
|
+
}
|
|
618
|
+
],
|
|
619
|
+
},
|
|
533
620
|
}
|
|
534
621
|
}
|
|
535
622
|
)
|
|
536
623
|
|
|
537
624
|
text: str
|
|
538
|
-
based_on:
|
|
625
|
+
based_on: ReflectBasedOn | None = Field(
|
|
626
|
+
default=None,
|
|
627
|
+
description="Evidence used to generate the response. Only present when include.facts is set.",
|
|
628
|
+
)
|
|
539
629
|
structured_output: dict | None = Field(
|
|
540
630
|
default=None,
|
|
541
631
|
description="Structured output parsed according to the request's response_schema. Only present when response_schema was provided in the request.",
|
|
@@ -544,6 +634,10 @@ class ReflectResponse(BaseModel):
|
|
|
544
634
|
default=None,
|
|
545
635
|
description="Token usage metrics for LLM calls during reflection.",
|
|
546
636
|
)
|
|
637
|
+
trace: ReflectTrace | None = Field(
|
|
638
|
+
default=None,
|
|
639
|
+
description="Execution trace of tool and LLM calls. Only present when include.tool_calls is set.",
|
|
640
|
+
)
|
|
547
641
|
|
|
548
642
|
|
|
549
643
|
class BanksResponse(BaseModel):
|
|
@@ -573,7 +667,7 @@ class BankProfileResponse(BaseModel):
|
|
|
573
667
|
"bank_id": "user123",
|
|
574
668
|
"name": "Alice",
|
|
575
669
|
"disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
|
|
576
|
-
"
|
|
670
|
+
"mission": "I am a software engineer helping my team stay organized and ship quality code",
|
|
577
671
|
}
|
|
578
672
|
}
|
|
579
673
|
)
|
|
@@ -581,7 +675,9 @@ class BankProfileResponse(BaseModel):
|
|
|
581
675
|
bank_id: str
|
|
582
676
|
name: str
|
|
583
677
|
disposition: DispositionTraits
|
|
584
|
-
|
|
678
|
+
mission: str = Field(description="The agent's mission - who they are and what they're trying to accomplish")
|
|
679
|
+
# Deprecated: use mission instead. Kept for backwards compatibility.
|
|
680
|
+
background: str | None = Field(default=None, description="Deprecated: use mission instead")
|
|
585
681
|
|
|
586
682
|
|
|
587
683
|
class UpdateDispositionRequest(BaseModel):
|
|
@@ -590,8 +686,32 @@ class UpdateDispositionRequest(BaseModel):
|
|
|
590
686
|
disposition: DispositionTraits
|
|
591
687
|
|
|
592
688
|
|
|
689
|
+
class SetMissionRequest(BaseModel):
|
|
690
|
+
"""Request model for setting/updating the agent's mission."""
|
|
691
|
+
|
|
692
|
+
model_config = ConfigDict(
|
|
693
|
+
json_schema_extra={"example": {"content": "I am a PM helping my engineering team stay organized"}}
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
content: str = Field(description="The mission content - who you are and what you're trying to accomplish")
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
class MissionResponse(BaseModel):
|
|
700
|
+
"""Response model for mission update."""
|
|
701
|
+
|
|
702
|
+
model_config = ConfigDict(
|
|
703
|
+
json_schema_extra={
|
|
704
|
+
"example": {
|
|
705
|
+
"mission": "I am a PM helping my engineering team stay organized and ship quality code.",
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
mission: str
|
|
711
|
+
|
|
712
|
+
|
|
593
713
|
class AddBackgroundRequest(BaseModel):
|
|
594
|
-
"""Request model for adding/merging background information."""
|
|
714
|
+
"""Request model for adding/merging background information. Deprecated: use SetMissionRequest instead."""
|
|
595
715
|
|
|
596
716
|
model_config = ConfigDict(
|
|
597
717
|
json_schema_extra={"example": {"content": "I was born in Texas", "update_disposition": True}}
|
|
@@ -599,23 +719,24 @@ class AddBackgroundRequest(BaseModel):
|
|
|
599
719
|
|
|
600
720
|
content: str = Field(description="New background information to add or merge")
|
|
601
721
|
update_disposition: bool = Field(
|
|
602
|
-
default=True, description="
|
|
722
|
+
default=True, description="Deprecated - disposition is no longer auto-inferred from mission"
|
|
603
723
|
)
|
|
604
724
|
|
|
605
725
|
|
|
606
726
|
class BackgroundResponse(BaseModel):
|
|
607
|
-
"""Response model for background update."""
|
|
727
|
+
"""Response model for background update. Deprecated: use MissionResponse instead."""
|
|
608
728
|
|
|
609
729
|
model_config = ConfigDict(
|
|
610
730
|
json_schema_extra={
|
|
611
731
|
"example": {
|
|
612
|
-
"
|
|
613
|
-
"disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
|
|
732
|
+
"mission": "I was born in Texas. I am a software engineer with 10 years of experience.",
|
|
614
733
|
}
|
|
615
734
|
}
|
|
616
735
|
)
|
|
617
736
|
|
|
618
|
-
|
|
737
|
+
mission: str
|
|
738
|
+
# Deprecated fields kept for backwards compatibility
|
|
739
|
+
background: str | None = Field(default=None, description="Deprecated: same as mission")
|
|
619
740
|
disposition: DispositionTraits | None = None
|
|
620
741
|
|
|
621
742
|
|
|
@@ -625,7 +746,7 @@ class BankListItem(BaseModel):
|
|
|
625
746
|
bank_id: str
|
|
626
747
|
name: str | None = None
|
|
627
748
|
disposition: DispositionTraits
|
|
628
|
-
|
|
749
|
+
mission: str | None = None
|
|
629
750
|
created_at: str | None = None
|
|
630
751
|
updated_at: str | None = None
|
|
631
752
|
|
|
@@ -641,7 +762,7 @@ class BankListResponse(BaseModel):
|
|
|
641
762
|
"bank_id": "user123",
|
|
642
763
|
"name": "Alice",
|
|
643
764
|
"disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
|
|
644
|
-
"
|
|
765
|
+
"mission": "I am a software engineer helping my team ship quality code",
|
|
645
766
|
"created_at": "2024-01-15T10:30:00Z",
|
|
646
767
|
"updated_at": "2024-01-16T14:20:00Z",
|
|
647
768
|
}
|
|
@@ -661,14 +782,16 @@ class CreateBankRequest(BaseModel):
|
|
|
661
782
|
"example": {
|
|
662
783
|
"name": "Alice",
|
|
663
784
|
"disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
|
|
664
|
-
"
|
|
785
|
+
"mission": "I am a PM helping my engineering team stay organized",
|
|
665
786
|
}
|
|
666
787
|
}
|
|
667
788
|
)
|
|
668
789
|
|
|
669
790
|
name: str | None = None
|
|
670
791
|
disposition: DispositionTraits | None = None
|
|
671
|
-
|
|
792
|
+
mission: str | None = Field(default=None, description="The agent's mission")
|
|
793
|
+
# Deprecated: use mission instead
|
|
794
|
+
background: str | None = Field(default=None, description="Deprecated: use mission instead")
|
|
672
795
|
|
|
673
796
|
|
|
674
797
|
class GraphDataResponse(BaseModel):
|
|
@@ -894,6 +1017,9 @@ class BankStatsResponse(BaseModel):
|
|
|
894
1017
|
"links_breakdown": {"fact": {"temporal": 100, "semantic": 60, "entity": 40}},
|
|
895
1018
|
"pending_operations": 2,
|
|
896
1019
|
"failed_operations": 0,
|
|
1020
|
+
"last_consolidated_at": "2024-01-15T10:30:00Z",
|
|
1021
|
+
"pending_consolidation": 0,
|
|
1022
|
+
"total_observations": 45,
|
|
897
1023
|
}
|
|
898
1024
|
}
|
|
899
1025
|
)
|
|
@@ -908,6 +1034,156 @@ class BankStatsResponse(BaseModel):
|
|
|
908
1034
|
links_breakdown: dict[str, dict[str, int]]
|
|
909
1035
|
pending_operations: int
|
|
910
1036
|
failed_operations: int
|
|
1037
|
+
# Consolidation stats
|
|
1038
|
+
last_consolidated_at: str | None = Field(default=None, description="When consolidation last ran (ISO format)")
|
|
1039
|
+
pending_consolidation: int = Field(default=0, description="Number of memories not yet processed into observations")
|
|
1040
|
+
total_observations: int = Field(default=0, description="Total number of observations")
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
# Mental Model models
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
class ObservationEvidenceResponse(BaseModel):
|
|
1047
|
+
"""A single piece of evidence supporting an observation."""
|
|
1048
|
+
|
|
1049
|
+
memory_id: str = Field(description="ID of the memory unit this evidence comes from")
|
|
1050
|
+
quote: str = Field(description="Exact quote from the memory supporting the observation")
|
|
1051
|
+
relevance: str = Field(description="Brief explanation of how this quote supports the observation")
|
|
1052
|
+
timestamp: str = Field(description="When the source memory was created (ISO format)")
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
# =========================================================================
|
|
1056
|
+
# Directive Models
|
|
1057
|
+
# =========================================================================
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
class DirectiveResponse(BaseModel):
|
|
1061
|
+
"""Response model for a directive."""
|
|
1062
|
+
|
|
1063
|
+
id: str
|
|
1064
|
+
bank_id: str
|
|
1065
|
+
name: str
|
|
1066
|
+
content: str
|
|
1067
|
+
priority: int = 0
|
|
1068
|
+
is_active: bool = True
|
|
1069
|
+
tags: list[str] = Field(default_factory=list)
|
|
1070
|
+
created_at: str | None = None
|
|
1071
|
+
updated_at: str | None = None
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
class DirectiveListResponse(BaseModel):
|
|
1075
|
+
"""Response model for listing directives."""
|
|
1076
|
+
|
|
1077
|
+
items: list[DirectiveResponse]
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
class CreateDirectiveRequest(BaseModel):
|
|
1081
|
+
"""Request model for creating a directive."""
|
|
1082
|
+
|
|
1083
|
+
name: str = Field(description="Human-readable name for the directive")
|
|
1084
|
+
content: str = Field(description="The directive text to inject into prompts")
|
|
1085
|
+
priority: int = Field(default=0, description="Higher priority directives are injected first")
|
|
1086
|
+
is_active: bool = Field(default=True, description="Whether this directive is active")
|
|
1087
|
+
tags: list[str] = Field(default_factory=list, description="Tags for filtering")
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
class UpdateDirectiveRequest(BaseModel):
|
|
1091
|
+
"""Request model for updating a directive."""
|
|
1092
|
+
|
|
1093
|
+
name: str | None = Field(default=None, description="New name")
|
|
1094
|
+
content: str | None = Field(default=None, description="New content")
|
|
1095
|
+
priority: int | None = Field(default=None, description="New priority")
|
|
1096
|
+
is_active: bool | None = Field(default=None, description="New active status")
|
|
1097
|
+
tags: list[str] | None = Field(default=None, description="New tags")
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
# =========================================================================
|
|
1101
|
+
# Mental Models (stored reflect responses)
|
|
1102
|
+
# =========================================================================
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
class MentalModelTrigger(BaseModel):
|
|
1106
|
+
"""Trigger settings for a mental model."""
|
|
1107
|
+
|
|
1108
|
+
refresh_after_consolidation: bool = Field(
|
|
1109
|
+
default=False,
|
|
1110
|
+
description="If true, refresh this mental model after observations consolidation (real-time mode)",
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
class MentalModelResponse(BaseModel):
|
|
1115
|
+
"""Response model for a mental model (stored reflect response)."""
|
|
1116
|
+
|
|
1117
|
+
id: str
|
|
1118
|
+
bank_id: str
|
|
1119
|
+
name: str
|
|
1120
|
+
source_query: str
|
|
1121
|
+
content: str
|
|
1122
|
+
tags: list[str] = Field(default_factory=list)
|
|
1123
|
+
max_tokens: int = Field(default=2048)
|
|
1124
|
+
trigger: MentalModelTrigger = Field(default_factory=MentalModelTrigger)
|
|
1125
|
+
last_refreshed_at: str | None = None
|
|
1126
|
+
created_at: str | None = None
|
|
1127
|
+
reflect_response: dict | None = Field(
|
|
1128
|
+
default=None,
|
|
1129
|
+
description="Full reflect API response payload including based_on facts and observations",
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
class MentalModelListResponse(BaseModel):
|
|
1134
|
+
"""Response model for listing mental models."""
|
|
1135
|
+
|
|
1136
|
+
items: list[MentalModelResponse]
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
class CreateMentalModelRequest(BaseModel):
|
|
1140
|
+
"""Request model for creating a mental model."""
|
|
1141
|
+
|
|
1142
|
+
model_config = ConfigDict(
|
|
1143
|
+
json_schema_extra={
|
|
1144
|
+
"example": {
|
|
1145
|
+
"name": "Team Communication Preferences",
|
|
1146
|
+
"source_query": "How does the team prefer to communicate?",
|
|
1147
|
+
"tags": ["team"],
|
|
1148
|
+
"max_tokens": 2048,
|
|
1149
|
+
"trigger": {"refresh_after_consolidation": False},
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
name: str = Field(description="Human-readable name for the mental model")
|
|
1155
|
+
source_query: str = Field(description="The query to run to generate content")
|
|
1156
|
+
tags: list[str] = Field(default_factory=list, description="Tags for scoped visibility")
|
|
1157
|
+
max_tokens: int = Field(default=2048, ge=256, le=8192, description="Maximum tokens for generated content")
|
|
1158
|
+
trigger: MentalModelTrigger = Field(default_factory=MentalModelTrigger, description="Trigger settings")
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
class CreateMentalModelResponse(BaseModel):
|
|
1162
|
+
"""Response model for mental model creation."""
|
|
1163
|
+
|
|
1164
|
+
operation_id: str = Field(description="Operation ID to track progress")
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
class UpdateMentalModelRequest(BaseModel):
|
|
1168
|
+
"""Request model for updating a mental model."""
|
|
1169
|
+
|
|
1170
|
+
model_config = ConfigDict(
|
|
1171
|
+
json_schema_extra={
|
|
1172
|
+
"example": {
|
|
1173
|
+
"name": "Updated Team Communication Preferences",
|
|
1174
|
+
"source_query": "How does the team prefer to communicate?",
|
|
1175
|
+
"max_tokens": 4096,
|
|
1176
|
+
"tags": ["team", "communication"],
|
|
1177
|
+
"trigger": {"refresh_after_consolidation": True},
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
name: str | None = Field(default=None, description="New name for the mental model")
|
|
1183
|
+
source_query: str | None = Field(default=None, description="New source query for the mental model")
|
|
1184
|
+
max_tokens: int | None = Field(default=None, ge=256, le=8192, description="Maximum tokens for generated content")
|
|
1185
|
+
tags: list[str] | None = Field(default=None, description="Tags for scoped visibility")
|
|
1186
|
+
trigger: MentalModelTrigger | None = Field(default=None, description="Trigger settings")
|
|
911
1187
|
|
|
912
1188
|
|
|
913
1189
|
class OperationResponse(BaseModel):
|
|
@@ -919,7 +1195,7 @@ class OperationResponse(BaseModel):
|
|
|
919
1195
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
920
1196
|
"task_type": "retain",
|
|
921
1197
|
"items_count": 5,
|
|
922
|
-
"document_id":
|
|
1198
|
+
"document_id": None,
|
|
923
1199
|
"created_at": "2024-01-15T10:30:00Z",
|
|
924
1200
|
"status": "pending",
|
|
925
1201
|
"error_message": None,
|
|
@@ -930,12 +1206,19 @@ class OperationResponse(BaseModel):
|
|
|
930
1206
|
id: str
|
|
931
1207
|
task_type: str
|
|
932
1208
|
items_count: int
|
|
933
|
-
document_id: str | None
|
|
1209
|
+
document_id: str | None = None
|
|
934
1210
|
created_at: str
|
|
935
1211
|
status: str
|
|
936
1212
|
error_message: str | None
|
|
937
1213
|
|
|
938
1214
|
|
|
1215
|
+
class ConsolidationResponse(BaseModel):
|
|
1216
|
+
"""Response model for consolidation trigger endpoint."""
|
|
1217
|
+
|
|
1218
|
+
operation_id: str = Field(description="ID of the async consolidation operation")
|
|
1219
|
+
deduplicated: bool = Field(default=False, description="True if an existing pending task was reused")
|
|
1220
|
+
|
|
1221
|
+
|
|
939
1222
|
class OperationsListResponse(BaseModel):
|
|
940
1223
|
"""Response model for list operations endpoint."""
|
|
941
1224
|
|
|
@@ -943,12 +1226,13 @@ class OperationsListResponse(BaseModel):
|
|
|
943
1226
|
json_schema_extra={
|
|
944
1227
|
"example": {
|
|
945
1228
|
"bank_id": "user123",
|
|
1229
|
+
"total": 150,
|
|
1230
|
+
"limit": 20,
|
|
1231
|
+
"offset": 0,
|
|
946
1232
|
"operations": [
|
|
947
1233
|
{
|
|
948
1234
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
949
1235
|
"task_type": "retain",
|
|
950
|
-
"items_count": 5,
|
|
951
|
-
"document_id": None,
|
|
952
1236
|
"created_at": "2024-01-15T10:30:00Z",
|
|
953
1237
|
"status": "pending",
|
|
954
1238
|
"error_message": None,
|
|
@@ -959,6 +1243,9 @@ class OperationsListResponse(BaseModel):
|
|
|
959
1243
|
)
|
|
960
1244
|
|
|
961
1245
|
bank_id: str
|
|
1246
|
+
total: int
|
|
1247
|
+
limit: int
|
|
1248
|
+
offset: int
|
|
962
1249
|
operations: list[OperationResponse]
|
|
963
1250
|
|
|
964
1251
|
|
|
@@ -980,6 +1267,76 @@ class CancelOperationResponse(BaseModel):
|
|
|
980
1267
|
operation_id: str
|
|
981
1268
|
|
|
982
1269
|
|
|
1270
|
+
class OperationStatusResponse(BaseModel):
|
|
1271
|
+
"""Response model for getting a single operation status."""
|
|
1272
|
+
|
|
1273
|
+
model_config = ConfigDict(
|
|
1274
|
+
json_schema_extra={
|
|
1275
|
+
"example": {
|
|
1276
|
+
"operation_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
1277
|
+
"status": "completed",
|
|
1278
|
+
"operation_type": "refresh_mental_models",
|
|
1279
|
+
"created_at": "2024-01-15T10:30:00Z",
|
|
1280
|
+
"updated_at": "2024-01-15T10:31:30Z",
|
|
1281
|
+
"completed_at": "2024-01-15T10:31:30Z",
|
|
1282
|
+
"error_message": None,
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
)
|
|
1286
|
+
|
|
1287
|
+
operation_id: str
|
|
1288
|
+
status: Literal["pending", "completed", "failed", "not_found"]
|
|
1289
|
+
operation_type: str | None = None
|
|
1290
|
+
created_at: str | None = None
|
|
1291
|
+
updated_at: str | None = None
|
|
1292
|
+
completed_at: str | None = None
|
|
1293
|
+
error_message: str | None = None
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
class AsyncOperationSubmitResponse(BaseModel):
|
|
1297
|
+
"""Response model for submitting an async operation."""
|
|
1298
|
+
|
|
1299
|
+
model_config = ConfigDict(
|
|
1300
|
+
json_schema_extra={
|
|
1301
|
+
"example": {
|
|
1302
|
+
"operation_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
1303
|
+
"status": "queued",
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
operation_id: str
|
|
1309
|
+
status: str
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
class FeaturesInfo(BaseModel):
|
|
1313
|
+
"""Feature flags indicating which capabilities are enabled."""
|
|
1314
|
+
|
|
1315
|
+
observations: bool = Field(description="Whether observations (auto-consolidation) are enabled")
|
|
1316
|
+
mcp: bool = Field(description="Whether MCP (Model Context Protocol) server is enabled")
|
|
1317
|
+
worker: bool = Field(description="Whether the background worker is enabled")
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
class VersionResponse(BaseModel):
|
|
1321
|
+
"""Response model for the version/info endpoint."""
|
|
1322
|
+
|
|
1323
|
+
model_config = ConfigDict(
|
|
1324
|
+
json_schema_extra={
|
|
1325
|
+
"example": {
|
|
1326
|
+
"api_version": "0.4.0",
|
|
1327
|
+
"features": {
|
|
1328
|
+
"observations": False,
|
|
1329
|
+
"mcp": True,
|
|
1330
|
+
"worker": True,
|
|
1331
|
+
},
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
api_version: str = Field(description="API version string")
|
|
1337
|
+
features: FeaturesInfo = Field(description="Enabled feature flags")
|
|
1338
|
+
|
|
1339
|
+
|
|
983
1340
|
def create_app(
|
|
984
1341
|
memory: MemoryEngine,
|
|
985
1342
|
initialize_memory: bool = True,
|
|
@@ -1015,6 +1372,16 @@ def create_app(
|
|
|
1015
1372
|
Lifespan context manager for startup and shutdown events.
|
|
1016
1373
|
Note: This only fires when running the app standalone, not when mounted.
|
|
1017
1374
|
"""
|
|
1375
|
+
import asyncio
|
|
1376
|
+
import socket
|
|
1377
|
+
|
|
1378
|
+
from hindsight_api.config import get_config
|
|
1379
|
+
from hindsight_api.worker import WorkerPoller
|
|
1380
|
+
|
|
1381
|
+
config = get_config()
|
|
1382
|
+
poller = None
|
|
1383
|
+
poller_task = None
|
|
1384
|
+
|
|
1018
1385
|
# Initialize OpenTelemetry metrics
|
|
1019
1386
|
try:
|
|
1020
1387
|
prometheus_reader = initialize_metrics(service_name="hindsight-api", service_version="1.0.0")
|
|
@@ -1037,6 +1404,21 @@ def create_app(
|
|
|
1037
1404
|
metrics_collector.set_db_pool(memory._pool)
|
|
1038
1405
|
logging.info("DB pool metrics configured")
|
|
1039
1406
|
|
|
1407
|
+
# Start worker poller if enabled (standalone mode)
|
|
1408
|
+
if config.worker_enabled and memory._pool is not None:
|
|
1409
|
+
worker_id = config.worker_id or socket.gethostname()
|
|
1410
|
+
poller = WorkerPoller(
|
|
1411
|
+
pool=memory._pool,
|
|
1412
|
+
worker_id=worker_id,
|
|
1413
|
+
executor=memory.execute_task,
|
|
1414
|
+
poll_interval_ms=config.worker_poll_interval_ms,
|
|
1415
|
+
batch_size=config.worker_batch_size,
|
|
1416
|
+
max_retries=config.worker_max_retries,
|
|
1417
|
+
tenant_extension=getattr(memory, "_tenant_extension", None),
|
|
1418
|
+
)
|
|
1419
|
+
poller_task = asyncio.create_task(poller.run())
|
|
1420
|
+
logging.info(f"Worker poller started (worker_id={worker_id})")
|
|
1421
|
+
|
|
1040
1422
|
# Call HTTP extension startup hook
|
|
1041
1423
|
if http_extension:
|
|
1042
1424
|
await http_extension.on_startup()
|
|
@@ -1044,6 +1426,17 @@ def create_app(
|
|
|
1044
1426
|
|
|
1045
1427
|
yield
|
|
1046
1428
|
|
|
1429
|
+
# Shutdown worker poller if running
|
|
1430
|
+
if poller is not None:
|
|
1431
|
+
await poller.shutdown_graceful(timeout=30.0)
|
|
1432
|
+
if poller_task is not None:
|
|
1433
|
+
poller_task.cancel()
|
|
1434
|
+
try:
|
|
1435
|
+
await poller_task
|
|
1436
|
+
except asyncio.CancelledError:
|
|
1437
|
+
pass
|
|
1438
|
+
logging.info("Worker poller stopped")
|
|
1439
|
+
|
|
1047
1440
|
# Call HTTP extension shutdown hook
|
|
1048
1441
|
if http_extension:
|
|
1049
1442
|
await http_extension.on_shutdown()
|
|
@@ -1158,6 +1551,35 @@ def _register_routes(app: FastAPI):
|
|
|
1158
1551
|
status_code = 200 if health.get("status") == "healthy" else 503
|
|
1159
1552
|
return JSONResponse(content=health, status_code=status_code)
|
|
1160
1553
|
|
|
1554
|
+
@app.get(
|
|
1555
|
+
"/version",
|
|
1556
|
+
response_model=VersionResponse,
|
|
1557
|
+
summary="Get API version and feature flags",
|
|
1558
|
+
description="Returns API version information and enabled feature flags. "
|
|
1559
|
+
"Use this to check which capabilities are available in this deployment.",
|
|
1560
|
+
tags=["Monitoring"],
|
|
1561
|
+
operation_id="get_version",
|
|
1562
|
+
)
|
|
1563
|
+
async def version_endpoint() -> VersionResponse:
|
|
1564
|
+
"""
|
|
1565
|
+
Get API version and enabled features.
|
|
1566
|
+
|
|
1567
|
+
Returns version info and feature flags that can be used by clients
|
|
1568
|
+
to determine which capabilities are available.
|
|
1569
|
+
"""
|
|
1570
|
+
from hindsight_api import __version__
|
|
1571
|
+
from hindsight_api.config import get_config
|
|
1572
|
+
|
|
1573
|
+
config = get_config()
|
|
1574
|
+
return VersionResponse(
|
|
1575
|
+
api_version=__version__,
|
|
1576
|
+
features=FeaturesInfo(
|
|
1577
|
+
observations=config.enable_observations,
|
|
1578
|
+
mcp=config.mcp_enabled,
|
|
1579
|
+
worker=config.worker_enabled,
|
|
1580
|
+
),
|
|
1581
|
+
)
|
|
1582
|
+
|
|
1161
1583
|
@app.get(
|
|
1162
1584
|
"/metrics",
|
|
1163
1585
|
summary="Prometheus metrics endpoint",
|
|
@@ -1301,8 +1723,10 @@ def _register_routes(app: FastAPI):
|
|
|
1301
1723
|
metrics = get_metrics_collector()
|
|
1302
1724
|
|
|
1303
1725
|
try:
|
|
1304
|
-
# Default to world
|
|
1726
|
+
# Default to world and experience if not specified (exclude observation and opinion)
|
|
1727
|
+
# Filter out 'opinion' even if requested - opinions are excluded from recall
|
|
1305
1728
|
fact_types = request.types if request.types else list(VALID_RECALL_FACT_TYPES)
|
|
1729
|
+
fact_types = [ft for ft in fact_types if ft != "opinion"]
|
|
1306
1730
|
|
|
1307
1731
|
# Parse query_timestamp if provided
|
|
1308
1732
|
question_date = None
|
|
@@ -1391,7 +1815,10 @@ def _register_routes(app: FastAPI):
|
|
|
1391
1815
|
)
|
|
1392
1816
|
|
|
1393
1817
|
response = RecallResponse(
|
|
1394
|
-
results=recall_results,
|
|
1818
|
+
results=recall_results,
|
|
1819
|
+
trace=core_result.trace,
|
|
1820
|
+
entities=entities_response,
|
|
1821
|
+
chunks=chunks_response,
|
|
1395
1822
|
)
|
|
1396
1823
|
|
|
1397
1824
|
handler_duration = time.time() - handler_start
|
|
@@ -1442,13 +1869,18 @@ def _register_routes(app: FastAPI):
|
|
|
1442
1869
|
metrics = get_metrics_collector()
|
|
1443
1870
|
|
|
1444
1871
|
try:
|
|
1872
|
+
# Handle deprecated context field by concatenating with query
|
|
1873
|
+
query = request.query
|
|
1874
|
+
if request.context:
|
|
1875
|
+
query = f"{request.query}\n\nAdditional context: {request.context}"
|
|
1876
|
+
|
|
1445
1877
|
# Use the memory system's reflect_async method (record metrics)
|
|
1446
1878
|
with metrics.record_operation("reflect", bank_id=bank_id, source="api", budget=request.budget.value):
|
|
1447
1879
|
core_result = await app.state.memory.reflect_async(
|
|
1448
1880
|
bank_id=bank_id,
|
|
1449
|
-
query=
|
|
1881
|
+
query=query,
|
|
1450
1882
|
budget=request.budget,
|
|
1451
|
-
context=
|
|
1883
|
+
context=None, # Deprecated, now concatenated with query
|
|
1452
1884
|
max_tokens=request.max_tokens,
|
|
1453
1885
|
response_schema=request.response_schema,
|
|
1454
1886
|
request_context=request_context,
|
|
@@ -1456,27 +1888,73 @@ def _register_routes(app: FastAPI):
|
|
|
1456
1888
|
tags_match=request.tags_match,
|
|
1457
1889
|
)
|
|
1458
1890
|
|
|
1459
|
-
#
|
|
1460
|
-
|
|
1891
|
+
# Build based_on (memories + mental_models + directives) if facts are requested
|
|
1892
|
+
based_on_result: ReflectBasedOn | None = None
|
|
1461
1893
|
if request.include.facts is not None:
|
|
1894
|
+
memories = []
|
|
1895
|
+
mental_models = []
|
|
1896
|
+
directives = []
|
|
1462
1897
|
for fact_type, facts in core_result.based_on.items():
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1898
|
+
if fact_type == "directives":
|
|
1899
|
+
# Directives have different structure (id, name, content)
|
|
1900
|
+
for directive in facts:
|
|
1901
|
+
directives.append(
|
|
1902
|
+
ReflectDirective(
|
|
1903
|
+
id=directive.id,
|
|
1904
|
+
name=directive.name,
|
|
1905
|
+
content=directive.content,
|
|
1906
|
+
)
|
|
1907
|
+
)
|
|
1908
|
+
elif fact_type == "mental_models":
|
|
1909
|
+
# Mental models are MemoryFact with type "mental_models"
|
|
1910
|
+
for fact in facts:
|
|
1911
|
+
mental_models.append(
|
|
1912
|
+
ReflectMentalModel(
|
|
1913
|
+
id=fact.id,
|
|
1914
|
+
text=fact.text,
|
|
1915
|
+
context=fact.context,
|
|
1916
|
+
)
|
|
1472
1917
|
)
|
|
1473
|
-
|
|
1918
|
+
else:
|
|
1919
|
+
for fact in facts:
|
|
1920
|
+
memories.append(
|
|
1921
|
+
ReflectFact(
|
|
1922
|
+
id=fact.id,
|
|
1923
|
+
text=fact.text,
|
|
1924
|
+
type=fact.fact_type,
|
|
1925
|
+
context=fact.context,
|
|
1926
|
+
occurred_start=fact.occurred_start,
|
|
1927
|
+
occurred_end=fact.occurred_end,
|
|
1928
|
+
)
|
|
1929
|
+
)
|
|
1930
|
+
based_on_result = ReflectBasedOn(memories=memories, mental_models=mental_models, directives=directives)
|
|
1931
|
+
|
|
1932
|
+
# Build trace (tool_calls + llm_calls + observations) if tool_calls is requested
|
|
1933
|
+
trace_result: ReflectTrace | None = None
|
|
1934
|
+
if request.include.tool_calls is not None:
|
|
1935
|
+
include_output = request.include.tool_calls.output
|
|
1936
|
+
tool_calls = [
|
|
1937
|
+
ReflectToolCall(
|
|
1938
|
+
tool=tc.tool,
|
|
1939
|
+
input=tc.input,
|
|
1940
|
+
output=tc.output if include_output else None,
|
|
1941
|
+
duration_ms=tc.duration_ms,
|
|
1942
|
+
iteration=tc.iteration,
|
|
1943
|
+
)
|
|
1944
|
+
for tc in core_result.tool_trace
|
|
1945
|
+
]
|
|
1946
|
+
llm_calls = [ReflectLLMCall(scope=lc.scope, duration_ms=lc.duration_ms) for lc in core_result.llm_trace]
|
|
1947
|
+
trace_result = ReflectTrace(
|
|
1948
|
+
tool_calls=tool_calls,
|
|
1949
|
+
llm_calls=llm_calls,
|
|
1950
|
+
)
|
|
1474
1951
|
|
|
1475
1952
|
return ReflectResponse(
|
|
1476
1953
|
text=core_result.text,
|
|
1477
|
-
based_on=
|
|
1954
|
+
based_on=based_on_result,
|
|
1478
1955
|
structured_output=core_result.structured_output,
|
|
1479
1956
|
usage=core_result.usage,
|
|
1957
|
+
trace=trace_result,
|
|
1480
1958
|
)
|
|
1481
1959
|
|
|
1482
1960
|
except OperationValidationError as e:
|
|
@@ -1602,6 +2080,31 @@ def _register_routes(app: FastAPI):
|
|
|
1602
2080
|
)
|
|
1603
2081
|
total_documents = doc_count_result["count"] if doc_count_result else 0
|
|
1604
2082
|
|
|
2083
|
+
# Get consolidation stats from memory-level tracking
|
|
2084
|
+
consolidation_stats = await conn.fetchrow(
|
|
2085
|
+
f"""
|
|
2086
|
+
SELECT
|
|
2087
|
+
MAX(consolidated_at) as last_consolidated_at,
|
|
2088
|
+
COUNT(*) FILTER (WHERE consolidated_at IS NULL AND fact_type IN ('experience', 'world')) as pending
|
|
2089
|
+
FROM {fq_table("memory_units")}
|
|
2090
|
+
WHERE bank_id = $1
|
|
2091
|
+
""",
|
|
2092
|
+
bank_id,
|
|
2093
|
+
)
|
|
2094
|
+
last_consolidated_at = consolidation_stats["last_consolidated_at"] if consolidation_stats else None
|
|
2095
|
+
pending_consolidation = consolidation_stats["pending"] if consolidation_stats else 0
|
|
2096
|
+
|
|
2097
|
+
# Count total observations (consolidated knowledge)
|
|
2098
|
+
observation_count_result = await conn.fetchrow(
|
|
2099
|
+
f"""
|
|
2100
|
+
SELECT COUNT(*) as count
|
|
2101
|
+
FROM {fq_table("memory_units")}
|
|
2102
|
+
WHERE bank_id = $1 AND fact_type = 'observation'
|
|
2103
|
+
""",
|
|
2104
|
+
bank_id,
|
|
2105
|
+
)
|
|
2106
|
+
total_observations = observation_count_result["count"] if observation_count_result else 0
|
|
2107
|
+
|
|
1605
2108
|
# Format results
|
|
1606
2109
|
nodes_by_type = {row["fact_type"]: row["count"] for row in node_stats}
|
|
1607
2110
|
links_by_type = {row["link_type"]: row["count"] for row in link_stats}
|
|
@@ -1631,6 +2134,9 @@ def _register_routes(app: FastAPI):
|
|
|
1631
2134
|
links_breakdown=links_breakdown,
|
|
1632
2135
|
pending_operations=pending_operations,
|
|
1633
2136
|
failed_operations=failed_operations,
|
|
2137
|
+
last_consolidated_at=(last_consolidated_at.isoformat() if last_consolidated_at else None),
|
|
2138
|
+
pending_consolidation=pending_consolidation,
|
|
2139
|
+
total_observations=total_observations,
|
|
1634
2140
|
)
|
|
1635
2141
|
|
|
1636
2142
|
except (AuthenticationError, HTTPException):
|
|
@@ -1718,54 +2224,422 @@ def _register_routes(app: FastAPI):
|
|
|
1718
2224
|
@app.post(
|
|
1719
2225
|
"/v1/default/banks/{bank_id}/entities/{entity_id}/regenerate",
|
|
1720
2226
|
response_model=EntityDetailResponse,
|
|
1721
|
-
summary="Regenerate entity observations",
|
|
1722
|
-
description="
|
|
2227
|
+
summary="Regenerate entity observations (deprecated)",
|
|
2228
|
+
description="This endpoint is deprecated. Entity observations have been replaced by mental models.",
|
|
1723
2229
|
operation_id="regenerate_entity_observations",
|
|
1724
2230
|
tags=["Entities"],
|
|
2231
|
+
deprecated=True,
|
|
1725
2232
|
)
|
|
1726
2233
|
async def api_regenerate_entity_observations(
|
|
1727
2234
|
bank_id: str,
|
|
1728
2235
|
entity_id: str,
|
|
1729
2236
|
request_context: RequestContext = Depends(get_request_context),
|
|
1730
2237
|
):
|
|
1731
|
-
"""Regenerate observations for an entity."""
|
|
2238
|
+
"""Regenerate observations for an entity. DEPRECATED."""
|
|
2239
|
+
raise HTTPException(
|
|
2240
|
+
status_code=410,
|
|
2241
|
+
detail="This endpoint is deprecated. Entity observations are no longer supported.",
|
|
2242
|
+
)
|
|
2243
|
+
|
|
2244
|
+
# =========================================================================
|
|
2245
|
+
# =========================================================================
|
|
2246
|
+
# MENTAL MODELS ENDPOINTS (stored reflect responses)
|
|
2247
|
+
# =========================================================================
|
|
2248
|
+
|
|
2249
|
+
@app.get(
|
|
2250
|
+
"/v1/default/banks/{bank_id}/mental-models",
|
|
2251
|
+
response_model=MentalModelListResponse,
|
|
2252
|
+
summary="List mental models",
|
|
2253
|
+
description="List user-curated living documents that stay current.",
|
|
2254
|
+
operation_id="list_mental_models",
|
|
2255
|
+
tags=["Mental Models"],
|
|
2256
|
+
)
|
|
2257
|
+
async def api_list_mental_models(
|
|
2258
|
+
bank_id: str,
|
|
2259
|
+
tags_filter: list[str] | None = Query(None, alias="tags", description="Filter by tags"),
|
|
2260
|
+
tags_match: Literal["any", "all", "exact"] = Query("any", description="How to match tags"),
|
|
2261
|
+
limit: int = Query(100, ge=1, le=1000),
|
|
2262
|
+
offset: int = Query(0, ge=0),
|
|
2263
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2264
|
+
):
|
|
2265
|
+
"""List mental models for a bank."""
|
|
1732
2266
|
try:
|
|
1733
|
-
|
|
1734
|
-
|
|
2267
|
+
mental_models = await app.state.memory.list_mental_models(
|
|
2268
|
+
bank_id=bank_id,
|
|
2269
|
+
tags=tags_filter,
|
|
2270
|
+
tags_match=tags_match,
|
|
2271
|
+
limit=limit,
|
|
2272
|
+
offset=offset,
|
|
2273
|
+
request_context=request_context,
|
|
2274
|
+
)
|
|
2275
|
+
return MentalModelListResponse(items=[MentalModelResponse(**m) for m in mental_models])
|
|
2276
|
+
except (AuthenticationError, HTTPException):
|
|
2277
|
+
raise
|
|
2278
|
+
except Exception as e:
|
|
2279
|
+
import traceback
|
|
1735
2280
|
|
|
1736
|
-
|
|
1737
|
-
|
|
2281
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2282
|
+
logger.error(f"Error in GET /v1/default/banks/{bank_id}/mental-models: {error_detail}")
|
|
2283
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1738
2284
|
|
|
1739
|
-
|
|
1740
|
-
|
|
2285
|
+
@app.get(
|
|
2286
|
+
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}",
|
|
2287
|
+
response_model=MentalModelResponse,
|
|
2288
|
+
summary="Get mental model",
|
|
2289
|
+
description="Get a specific mental model by ID.",
|
|
2290
|
+
operation_id="get_mental_model",
|
|
2291
|
+
tags=["Mental Models"],
|
|
2292
|
+
)
|
|
2293
|
+
async def api_get_mental_model(
|
|
2294
|
+
bank_id: str,
|
|
2295
|
+
mental_model_id: str,
|
|
2296
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2297
|
+
):
|
|
2298
|
+
"""Get a mental model by ID."""
|
|
2299
|
+
try:
|
|
2300
|
+
mental_model = await app.state.memory.get_mental_model(
|
|
1741
2301
|
bank_id=bank_id,
|
|
1742
|
-
|
|
1743
|
-
entity_name=entity["canonical_name"],
|
|
2302
|
+
mental_model_id=mental_model_id,
|
|
1744
2303
|
request_context=request_context,
|
|
1745
2304
|
)
|
|
2305
|
+
if mental_model is None:
|
|
2306
|
+
raise HTTPException(status_code=404, detail=f"Mental model '{mental_model_id}' not found")
|
|
2307
|
+
return MentalModelResponse(**mental_model)
|
|
2308
|
+
except (AuthenticationError, HTTPException):
|
|
2309
|
+
raise
|
|
2310
|
+
except Exception as e:
|
|
2311
|
+
import traceback
|
|
1746
2312
|
|
|
1747
|
-
|
|
1748
|
-
|
|
2313
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2314
|
+
logger.error(f"Error in GET /v1/default/banks/{bank_id}/mental-models/{mental_model_id}: {error_detail}")
|
|
2315
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1749
2316
|
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2317
|
+
@app.post(
|
|
2318
|
+
"/v1/default/banks/{bank_id}/mental-models",
|
|
2319
|
+
response_model=CreateMentalModelResponse,
|
|
2320
|
+
summary="Create mental model",
|
|
2321
|
+
description="Create a mental model by running reflect with the source query in the background. "
|
|
2322
|
+
"Returns an operation ID to track progress. The content is auto-generated by the reflect endpoint. "
|
|
2323
|
+
"Use the operations endpoint to check completion status.",
|
|
2324
|
+
operation_id="create_mental_model",
|
|
2325
|
+
tags=["Mental Models"],
|
|
2326
|
+
)
|
|
2327
|
+
async def api_create_mental_model(
|
|
2328
|
+
bank_id: str,
|
|
2329
|
+
body: CreateMentalModelRequest,
|
|
2330
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2331
|
+
):
|
|
2332
|
+
"""Create a mental model (async - returns operation_id)."""
|
|
2333
|
+
try:
|
|
2334
|
+
# 1. Create the mental model with placeholder content
|
|
2335
|
+
mental_model = await app.state.memory.create_mental_model(
|
|
2336
|
+
bank_id=bank_id,
|
|
2337
|
+
name=body.name,
|
|
2338
|
+
source_query=body.source_query,
|
|
2339
|
+
content="Generating content...",
|
|
2340
|
+
tags=body.tags if body.tags else None,
|
|
2341
|
+
max_tokens=body.max_tokens,
|
|
2342
|
+
trigger=body.trigger.model_dump() if body.trigger else None,
|
|
2343
|
+
request_context=request_context,
|
|
2344
|
+
)
|
|
2345
|
+
# 2. Schedule a refresh to generate the actual content
|
|
2346
|
+
result = await app.state.memory.submit_async_refresh_mental_model(
|
|
2347
|
+
bank_id=bank_id,
|
|
2348
|
+
mental_model_id=mental_model["id"],
|
|
2349
|
+
request_context=request_context,
|
|
2350
|
+
)
|
|
2351
|
+
return CreateMentalModelResponse(operation_id=result["operation_id"])
|
|
2352
|
+
except ValueError as e:
|
|
2353
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
2354
|
+
except (AuthenticationError, HTTPException):
|
|
2355
|
+
raise
|
|
2356
|
+
except Exception as e:
|
|
2357
|
+
import traceback
|
|
2358
|
+
|
|
2359
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2360
|
+
logger.error(f"Error in POST /v1/default/banks/{bank_id}/mental-models: {error_detail}")
|
|
2361
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2362
|
+
|
|
2363
|
+
@app.post(
|
|
2364
|
+
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh",
|
|
2365
|
+
response_model=AsyncOperationSubmitResponse,
|
|
2366
|
+
summary="Refresh mental model",
|
|
2367
|
+
description="Submit an async task to re-run the source query through reflect and update the content.",
|
|
2368
|
+
operation_id="refresh_mental_model",
|
|
2369
|
+
tags=["Mental Models"],
|
|
2370
|
+
)
|
|
2371
|
+
async def api_refresh_mental_model(
|
|
2372
|
+
bank_id: str,
|
|
2373
|
+
mental_model_id: str,
|
|
2374
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2375
|
+
):
|
|
2376
|
+
"""Refresh a mental model by re-running its source query (async)."""
|
|
2377
|
+
try:
|
|
2378
|
+
result = await app.state.memory.submit_async_refresh_mental_model(
|
|
2379
|
+
bank_id=bank_id,
|
|
2380
|
+
mental_model_id=mental_model_id,
|
|
2381
|
+
request_context=request_context,
|
|
2382
|
+
)
|
|
2383
|
+
return AsyncOperationSubmitResponse(operation_id=result["operation_id"], status="queued")
|
|
2384
|
+
except ValueError as e:
|
|
2385
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
2386
|
+
except (AuthenticationError, HTTPException):
|
|
2387
|
+
raise
|
|
2388
|
+
except Exception as e:
|
|
2389
|
+
import traceback
|
|
2390
|
+
|
|
2391
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2392
|
+
logger.error(
|
|
2393
|
+
f"Error in POST /v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh: {error_detail}"
|
|
2394
|
+
)
|
|
2395
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2396
|
+
|
|
2397
|
+
@app.patch(
|
|
2398
|
+
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}",
|
|
2399
|
+
response_model=MentalModelResponse,
|
|
2400
|
+
summary="Update mental model",
|
|
2401
|
+
description="Update a mental model's name and/or source query.",
|
|
2402
|
+
operation_id="update_mental_model",
|
|
2403
|
+
tags=["Mental Models"],
|
|
2404
|
+
)
|
|
2405
|
+
async def api_update_mental_model(
|
|
2406
|
+
bank_id: str,
|
|
2407
|
+
mental_model_id: str,
|
|
2408
|
+
body: UpdateMentalModelRequest,
|
|
2409
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2410
|
+
):
|
|
2411
|
+
"""Update a mental model."""
|
|
2412
|
+
try:
|
|
2413
|
+
mental_model = await app.state.memory.update_mental_model(
|
|
2414
|
+
bank_id=bank_id,
|
|
2415
|
+
mental_model_id=mental_model_id,
|
|
2416
|
+
name=body.name,
|
|
2417
|
+
source_query=body.source_query,
|
|
2418
|
+
max_tokens=body.max_tokens,
|
|
2419
|
+
tags=body.tags,
|
|
2420
|
+
trigger=body.trigger.model_dump() if body.trigger else None,
|
|
2421
|
+
request_context=request_context,
|
|
2422
|
+
)
|
|
2423
|
+
if mental_model is None:
|
|
2424
|
+
raise HTTPException(status_code=404, detail=f"Mental model '{mental_model_id}' not found")
|
|
2425
|
+
return MentalModelResponse(**mental_model)
|
|
2426
|
+
except (AuthenticationError, HTTPException):
|
|
2427
|
+
raise
|
|
2428
|
+
except Exception as e:
|
|
2429
|
+
import traceback
|
|
2430
|
+
|
|
2431
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2432
|
+
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}/mental-models/{mental_model_id}: {error_detail}")
|
|
2433
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2434
|
+
|
|
2435
|
+
@app.delete(
|
|
2436
|
+
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}",
|
|
2437
|
+
summary="Delete mental model",
|
|
2438
|
+
description="Delete a mental model.",
|
|
2439
|
+
operation_id="delete_mental_model",
|
|
2440
|
+
tags=["Mental Models"],
|
|
2441
|
+
)
|
|
2442
|
+
async def api_delete_mental_model(
|
|
2443
|
+
bank_id: str,
|
|
2444
|
+
mental_model_id: str,
|
|
2445
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2446
|
+
):
|
|
2447
|
+
"""Delete a mental model."""
|
|
2448
|
+
try:
|
|
2449
|
+
deleted = await app.state.memory.delete_mental_model(
|
|
2450
|
+
bank_id=bank_id,
|
|
2451
|
+
mental_model_id=mental_model_id,
|
|
2452
|
+
request_context=request_context,
|
|
2453
|
+
)
|
|
2454
|
+
if not deleted:
|
|
2455
|
+
raise HTTPException(status_code=404, detail=f"Mental model '{mental_model_id}' not found")
|
|
2456
|
+
return {"status": "deleted"}
|
|
2457
|
+
except (AuthenticationError, HTTPException):
|
|
2458
|
+
raise
|
|
2459
|
+
except Exception as e:
|
|
2460
|
+
import traceback
|
|
2461
|
+
|
|
2462
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2463
|
+
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/mental-models/{mental_model_id}: {error_detail}")
|
|
2464
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2465
|
+
|
|
2466
|
+
# =========================================================================
|
|
2467
|
+
# DIRECTIVES ENDPOINTS
|
|
2468
|
+
# =========================================================================
|
|
2469
|
+
|
|
2470
|
+
@app.get(
|
|
2471
|
+
"/v1/default/banks/{bank_id}/directives",
|
|
2472
|
+
response_model=DirectiveListResponse,
|
|
2473
|
+
summary="List directives",
|
|
2474
|
+
description="List hard rules that are injected into prompts.",
|
|
2475
|
+
operation_id="list_directives",
|
|
2476
|
+
tags=["Directives"],
|
|
2477
|
+
)
|
|
2478
|
+
async def api_list_directives(
|
|
2479
|
+
bank_id: str,
|
|
2480
|
+
tags_filter: list[str] | None = Query(None, alias="tags", description="Filter by tags"),
|
|
2481
|
+
tags_match: Literal["any", "all", "exact"] = Query("any", description="How to match tags"),
|
|
2482
|
+
active_only: bool = Query(True, description="Only return active directives"),
|
|
2483
|
+
limit: int = Query(100, ge=1, le=1000),
|
|
2484
|
+
offset: int = Query(0, ge=0),
|
|
2485
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2486
|
+
):
|
|
2487
|
+
"""List directives for a bank."""
|
|
2488
|
+
try:
|
|
2489
|
+
directives = await app.state.memory.list_directives(
|
|
2490
|
+
bank_id=bank_id,
|
|
2491
|
+
tags=tags_filter,
|
|
2492
|
+
tags_match=tags_match,
|
|
2493
|
+
active_only=active_only,
|
|
2494
|
+
limit=limit,
|
|
2495
|
+
offset=offset,
|
|
2496
|
+
request_context=request_context,
|
|
2497
|
+
)
|
|
2498
|
+
return DirectiveListResponse(items=[DirectiveResponse(**d) for d in directives])
|
|
2499
|
+
except (AuthenticationError, HTTPException):
|
|
2500
|
+
raise
|
|
2501
|
+
except Exception as e:
|
|
2502
|
+
import traceback
|
|
2503
|
+
|
|
2504
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2505
|
+
logger.error(f"Error in GET /v1/default/banks/{bank_id}/directives: {error_detail}")
|
|
2506
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2507
|
+
|
|
2508
|
+
@app.get(
|
|
2509
|
+
"/v1/default/banks/{bank_id}/directives/{directive_id}",
|
|
2510
|
+
response_model=DirectiveResponse,
|
|
2511
|
+
summary="Get directive",
|
|
2512
|
+
description="Get a specific directive by ID.",
|
|
2513
|
+
operation_id="get_directive",
|
|
2514
|
+
tags=["Directives"],
|
|
2515
|
+
)
|
|
2516
|
+
async def api_get_directive(
|
|
2517
|
+
bank_id: str,
|
|
2518
|
+
directive_id: str,
|
|
2519
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2520
|
+
):
|
|
2521
|
+
"""Get a directive by ID."""
|
|
2522
|
+
try:
|
|
2523
|
+
directive = await app.state.memory.get_directive(
|
|
2524
|
+
bank_id=bank_id,
|
|
2525
|
+
directive_id=directive_id,
|
|
2526
|
+
request_context=request_context,
|
|
2527
|
+
)
|
|
2528
|
+
if directive is None:
|
|
2529
|
+
raise HTTPException(status_code=404, detail=f"Directive '{directive_id}' not found")
|
|
2530
|
+
return DirectiveResponse(**directive)
|
|
2531
|
+
except (AuthenticationError, HTTPException):
|
|
2532
|
+
raise
|
|
2533
|
+
except Exception as e:
|
|
2534
|
+
import traceback
|
|
2535
|
+
|
|
2536
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2537
|
+
logger.error(f"Error in GET /v1/default/banks/{bank_id}/directives/{directive_id}: {error_detail}")
|
|
2538
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2539
|
+
|
|
2540
|
+
@app.post(
|
|
2541
|
+
"/v1/default/banks/{bank_id}/directives",
|
|
2542
|
+
response_model=DirectiveResponse,
|
|
2543
|
+
summary="Create directive",
|
|
2544
|
+
description="Create a hard rule that will be injected into prompts.",
|
|
2545
|
+
operation_id="create_directive",
|
|
2546
|
+
tags=["Directives"],
|
|
2547
|
+
)
|
|
2548
|
+
async def api_create_directive(
|
|
2549
|
+
bank_id: str,
|
|
2550
|
+
body: CreateDirectiveRequest,
|
|
2551
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2552
|
+
):
|
|
2553
|
+
"""Create a directive."""
|
|
2554
|
+
try:
|
|
2555
|
+
directive = await app.state.memory.create_directive(
|
|
2556
|
+
bank_id=bank_id,
|
|
2557
|
+
name=body.name,
|
|
2558
|
+
content=body.content,
|
|
2559
|
+
priority=body.priority,
|
|
2560
|
+
is_active=body.is_active,
|
|
2561
|
+
tags=body.tags,
|
|
2562
|
+
request_context=request_context,
|
|
2563
|
+
)
|
|
2564
|
+
return DirectiveResponse(**directive)
|
|
2565
|
+
except ValueError as e:
|
|
2566
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
2567
|
+
except (AuthenticationError, HTTPException):
|
|
2568
|
+
raise
|
|
2569
|
+
except Exception as e:
|
|
2570
|
+
import traceback
|
|
2571
|
+
|
|
2572
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2573
|
+
logger.error(f"Error in POST /v1/default/banks/{bank_id}/directives: {error_detail}")
|
|
2574
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2575
|
+
|
|
2576
|
+
@app.patch(
|
|
2577
|
+
"/v1/default/banks/{bank_id}/directives/{directive_id}",
|
|
2578
|
+
response_model=DirectiveResponse,
|
|
2579
|
+
summary="Update directive",
|
|
2580
|
+
description="Update a directive's properties.",
|
|
2581
|
+
operation_id="update_directive",
|
|
2582
|
+
tags=["Directives"],
|
|
2583
|
+
)
|
|
2584
|
+
async def api_update_directive(
|
|
2585
|
+
bank_id: str,
|
|
2586
|
+
directive_id: str,
|
|
2587
|
+
body: UpdateDirectiveRequest,
|
|
2588
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2589
|
+
):
|
|
2590
|
+
"""Update a directive."""
|
|
2591
|
+
try:
|
|
2592
|
+
directive = await app.state.memory.update_directive(
|
|
2593
|
+
bank_id=bank_id,
|
|
2594
|
+
directive_id=directive_id,
|
|
2595
|
+
name=body.name,
|
|
2596
|
+
content=body.content,
|
|
2597
|
+
priority=body.priority,
|
|
2598
|
+
is_active=body.is_active,
|
|
2599
|
+
tags=body.tags,
|
|
2600
|
+
request_context=request_context,
|
|
2601
|
+
)
|
|
2602
|
+
if directive is None:
|
|
2603
|
+
raise HTTPException(status_code=404, detail=f"Directive '{directive_id}' not found")
|
|
2604
|
+
return DirectiveResponse(**directive)
|
|
2605
|
+
except (AuthenticationError, HTTPException):
|
|
2606
|
+
raise
|
|
2607
|
+
except Exception as e:
|
|
2608
|
+
import traceback
|
|
2609
|
+
|
|
2610
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2611
|
+
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}/directives/{directive_id}: {error_detail}")
|
|
2612
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2613
|
+
|
|
2614
|
+
@app.delete(
|
|
2615
|
+
"/v1/default/banks/{bank_id}/directives/{directive_id}",
|
|
2616
|
+
summary="Delete directive",
|
|
2617
|
+
description="Delete a directive.",
|
|
2618
|
+
operation_id="delete_directive",
|
|
2619
|
+
tags=["Directives"],
|
|
2620
|
+
)
|
|
2621
|
+
async def api_delete_directive(
|
|
2622
|
+
bank_id: str,
|
|
2623
|
+
directive_id: str,
|
|
2624
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2625
|
+
):
|
|
2626
|
+
"""Delete a directive."""
|
|
2627
|
+
try:
|
|
2628
|
+
deleted = await app.state.memory.delete_directive(
|
|
2629
|
+
bank_id=bank_id,
|
|
2630
|
+
directive_id=directive_id,
|
|
2631
|
+
request_context=request_context,
|
|
1761
2632
|
)
|
|
2633
|
+
if not deleted:
|
|
2634
|
+
raise HTTPException(status_code=404, detail=f"Directive '{directive_id}' not found")
|
|
2635
|
+
return {"status": "deleted"}
|
|
1762
2636
|
except (AuthenticationError, HTTPException):
|
|
1763
2637
|
raise
|
|
1764
2638
|
except Exception as e:
|
|
1765
2639
|
import traceback
|
|
1766
2640
|
|
|
1767
2641
|
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
1768
|
-
logger.error(f"Error in /v1/default/banks/{bank_id}/
|
|
2642
|
+
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/directives/{directive_id}: {error_detail}")
|
|
1769
2643
|
raise HTTPException(status_code=500, detail=str(e))
|
|
1770
2644
|
|
|
1771
2645
|
@app.get(
|
|
@@ -1968,17 +2842,28 @@ def _register_routes(app: FastAPI):
|
|
|
1968
2842
|
"/v1/default/banks/{bank_id}/operations",
|
|
1969
2843
|
response_model=OperationsListResponse,
|
|
1970
2844
|
summary="List async operations",
|
|
1971
|
-
description="Get a list of
|
|
2845
|
+
description="Get a list of async operations for a specific agent, with optional filtering by status. Results are sorted by most recent first.",
|
|
1972
2846
|
operation_id="list_operations",
|
|
1973
2847
|
tags=["Operations"],
|
|
1974
2848
|
)
|
|
1975
|
-
async def api_list_operations(
|
|
1976
|
-
|
|
2849
|
+
async def api_list_operations(
|
|
2850
|
+
bank_id: str,
|
|
2851
|
+
status: str | None = Query(default=None, description="Filter by status: pending, completed, or failed"),
|
|
2852
|
+
limit: int = Query(default=20, ge=1, le=100, description="Maximum number of operations to return"),
|
|
2853
|
+
offset: int = Query(default=0, ge=0, description="Number of operations to skip"),
|
|
2854
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
2855
|
+
):
|
|
2856
|
+
"""List async operations for a memory bank with optional filtering and pagination."""
|
|
1977
2857
|
try:
|
|
1978
|
-
|
|
2858
|
+
result = await app.state.memory.list_operations(
|
|
2859
|
+
bank_id, status=status, limit=limit, offset=offset, request_context=request_context
|
|
2860
|
+
)
|
|
1979
2861
|
return OperationsListResponse(
|
|
1980
2862
|
bank_id=bank_id,
|
|
1981
|
-
|
|
2863
|
+
total=result["total"],
|
|
2864
|
+
limit=limit,
|
|
2865
|
+
offset=offset,
|
|
2866
|
+
operations=[OperationResponse(**op) for op in result["operations"]],
|
|
1982
2867
|
)
|
|
1983
2868
|
except (AuthenticationError, HTTPException):
|
|
1984
2869
|
raise
|
|
@@ -1989,6 +2874,37 @@ def _register_routes(app: FastAPI):
|
|
|
1989
2874
|
logger.error(f"Error in /v1/default/banks/{bank_id}/operations: {error_detail}")
|
|
1990
2875
|
raise HTTPException(status_code=500, detail=str(e))
|
|
1991
2876
|
|
|
2877
|
+
@app.get(
|
|
2878
|
+
"/v1/default/banks/{bank_id}/operations/{operation_id}",
|
|
2879
|
+
response_model=OperationStatusResponse,
|
|
2880
|
+
summary="Get operation status",
|
|
2881
|
+
description="Get the status of a specific async operation. Returns 'pending', 'completed', or 'failed'. "
|
|
2882
|
+
"Completed operations are removed from storage, so 'completed' means the operation finished successfully.",
|
|
2883
|
+
operation_id="get_operation_status",
|
|
2884
|
+
tags=["Operations"],
|
|
2885
|
+
)
|
|
2886
|
+
async def api_get_operation_status(
|
|
2887
|
+
bank_id: str, operation_id: str, request_context: RequestContext = Depends(get_request_context)
|
|
2888
|
+
):
|
|
2889
|
+
"""Get the status of an async operation."""
|
|
2890
|
+
try:
|
|
2891
|
+
# Validate UUID format
|
|
2892
|
+
try:
|
|
2893
|
+
uuid.UUID(operation_id)
|
|
2894
|
+
except ValueError:
|
|
2895
|
+
raise HTTPException(status_code=400, detail=f"Invalid operation_id format: {operation_id}")
|
|
2896
|
+
|
|
2897
|
+
result = await app.state.memory.get_operation_status(bank_id, operation_id, request_context=request_context)
|
|
2898
|
+
return OperationStatusResponse(**result)
|
|
2899
|
+
except (AuthenticationError, HTTPException):
|
|
2900
|
+
raise
|
|
2901
|
+
except Exception as e:
|
|
2902
|
+
import traceback
|
|
2903
|
+
|
|
2904
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
2905
|
+
logger.error(f"Error in GET /v1/default/banks/{bank_id}/operations/{operation_id}: {error_detail}")
|
|
2906
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
2907
|
+
|
|
1992
2908
|
@app.delete(
|
|
1993
2909
|
"/v1/default/banks/{bank_id}/operations/{operation_id}",
|
|
1994
2910
|
response_model=CancelOperationResponse,
|
|
@@ -2025,12 +2941,12 @@ def _register_routes(app: FastAPI):
|
|
|
2025
2941
|
"/v1/default/banks/{bank_id}/profile",
|
|
2026
2942
|
response_model=BankProfileResponse,
|
|
2027
2943
|
summary="Get memory bank profile",
|
|
2028
|
-
description="Get disposition traits and
|
|
2944
|
+
description="Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.",
|
|
2029
2945
|
operation_id="get_bank_profile",
|
|
2030
2946
|
tags=["Banks"],
|
|
2031
2947
|
)
|
|
2032
2948
|
async def api_get_bank_profile(bank_id: str, request_context: RequestContext = Depends(get_request_context)):
|
|
2033
|
-
"""Get memory bank profile (disposition +
|
|
2949
|
+
"""Get memory bank profile (disposition + mission)."""
|
|
2034
2950
|
try:
|
|
2035
2951
|
profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
|
2036
2952
|
# Convert DispositionTraits object to dict for Pydantic
|
|
@@ -2039,11 +2955,13 @@ def _register_routes(app: FastAPI):
|
|
|
2039
2955
|
if hasattr(profile["disposition"], "model_dump")
|
|
2040
2956
|
else dict(profile["disposition"])
|
|
2041
2957
|
)
|
|
2958
|
+
mission = profile.get("mission") or ""
|
|
2042
2959
|
return BankProfileResponse(
|
|
2043
2960
|
bank_id=bank_id,
|
|
2044
2961
|
name=profile["name"],
|
|
2045
2962
|
disposition=DispositionTraits(**disposition_dict),
|
|
2046
|
-
|
|
2963
|
+
mission=mission,
|
|
2964
|
+
background=mission, # Backwards compat
|
|
2047
2965
|
)
|
|
2048
2966
|
except (AuthenticationError, HTTPException):
|
|
2049
2967
|
raise
|
|
@@ -2079,11 +2997,13 @@ def _register_routes(app: FastAPI):
|
|
|
2079
2997
|
if hasattr(profile["disposition"], "model_dump")
|
|
2080
2998
|
else dict(profile["disposition"])
|
|
2081
2999
|
)
|
|
3000
|
+
mission = profile.get("mission") or ""
|
|
2082
3001
|
return BankProfileResponse(
|
|
2083
3002
|
bank_id=bank_id,
|
|
2084
3003
|
name=profile["name"],
|
|
2085
3004
|
disposition=DispositionTraits(**disposition_dict),
|
|
2086
|
-
|
|
3005
|
+
mission=mission,
|
|
3006
|
+
background=mission, # Backwards compat
|
|
2087
3007
|
)
|
|
2088
3008
|
except (AuthenticationError, HTTPException):
|
|
2089
3009
|
raise
|
|
@@ -2097,25 +3017,22 @@ def _register_routes(app: FastAPI):
|
|
|
2097
3017
|
@app.post(
|
|
2098
3018
|
"/v1/default/banks/{bank_id}/background",
|
|
2099
3019
|
response_model=BackgroundResponse,
|
|
2100
|
-
summary="Add/merge memory bank background",
|
|
2101
|
-
description="
|
|
3020
|
+
summary="Add/merge memory bank background (deprecated)",
|
|
3021
|
+
description="Deprecated: Use PUT /mission instead. This endpoint now updates the mission field.",
|
|
2102
3022
|
operation_id="add_bank_background",
|
|
2103
3023
|
tags=["Banks"],
|
|
3024
|
+
deprecated=True,
|
|
2104
3025
|
)
|
|
2105
3026
|
async def api_add_bank_background(
|
|
2106
3027
|
bank_id: str, request: AddBackgroundRequest, request_context: RequestContext = Depends(get_request_context)
|
|
2107
3028
|
):
|
|
2108
|
-
"""Add or merge bank background
|
|
3029
|
+
"""Deprecated: Add or merge bank background. Now updates mission field."""
|
|
2109
3030
|
try:
|
|
2110
|
-
result = await app.state.memory.
|
|
2111
|
-
bank_id, request.content,
|
|
3031
|
+
result = await app.state.memory.merge_bank_mission(
|
|
3032
|
+
bank_id, request.content, request_context=request_context
|
|
2112
3033
|
)
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
if "disposition" in result:
|
|
2116
|
-
response.disposition = DispositionTraits(**result["disposition"])
|
|
2117
|
-
|
|
2118
|
-
return response
|
|
3034
|
+
mission = result.get("mission") or ""
|
|
3035
|
+
return BackgroundResponse(mission=mission, background=mission)
|
|
2119
3036
|
except (AuthenticationError, HTTPException):
|
|
2120
3037
|
raise
|
|
2121
3038
|
except Exception as e:
|
|
@@ -2129,24 +3046,25 @@ def _register_routes(app: FastAPI):
|
|
|
2129
3046
|
"/v1/default/banks/{bank_id}",
|
|
2130
3047
|
response_model=BankProfileResponse,
|
|
2131
3048
|
summary="Create or update memory bank",
|
|
2132
|
-
description="Create a new agent or update existing agent with disposition and
|
|
3049
|
+
description="Create a new agent or update existing agent with disposition and mission. Auto-fills missing fields with defaults.",
|
|
2133
3050
|
operation_id="create_or_update_bank",
|
|
2134
3051
|
tags=["Banks"],
|
|
2135
3052
|
)
|
|
2136
3053
|
async def api_create_or_update_bank(
|
|
2137
3054
|
bank_id: str, request: CreateBankRequest, request_context: RequestContext = Depends(get_request_context)
|
|
2138
3055
|
):
|
|
2139
|
-
"""Create or update an agent with disposition and
|
|
3056
|
+
"""Create or update an agent with disposition and mission."""
|
|
2140
3057
|
try:
|
|
2141
3058
|
# Ensure bank exists by getting profile (auto-creates with defaults)
|
|
2142
3059
|
await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
|
2143
3060
|
|
|
2144
|
-
# Update name and/or
|
|
2145
|
-
|
|
3061
|
+
# Update name and/or mission if provided (support both mission and deprecated background)
|
|
3062
|
+
mission_value = request.mission or request.background
|
|
3063
|
+
if request.name is not None or mission_value is not None:
|
|
2146
3064
|
await app.state.memory.update_bank(
|
|
2147
3065
|
bank_id,
|
|
2148
3066
|
name=request.name,
|
|
2149
|
-
|
|
3067
|
+
mission=mission_value,
|
|
2150
3068
|
request_context=request_context,
|
|
2151
3069
|
)
|
|
2152
3070
|
|
|
@@ -2163,11 +3081,13 @@ def _register_routes(app: FastAPI):
|
|
|
2163
3081
|
if hasattr(final_profile["disposition"], "model_dump")
|
|
2164
3082
|
else dict(final_profile["disposition"])
|
|
2165
3083
|
)
|
|
3084
|
+
mission = final_profile.get("mission") or ""
|
|
2166
3085
|
return BankProfileResponse(
|
|
2167
3086
|
bank_id=bank_id,
|
|
2168
3087
|
name=final_profile["name"],
|
|
2169
3088
|
disposition=DispositionTraits(**disposition_dict),
|
|
2170
|
-
|
|
3089
|
+
mission=mission,
|
|
3090
|
+
background=mission, # Backwards compat
|
|
2171
3091
|
)
|
|
2172
3092
|
except (AuthenticationError, HTTPException):
|
|
2173
3093
|
raise
|
|
@@ -2178,6 +3098,62 @@ def _register_routes(app: FastAPI):
|
|
|
2178
3098
|
logger.error(f"Error in /v1/default/banks/{bank_id}: {error_detail}")
|
|
2179
3099
|
raise HTTPException(status_code=500, detail=str(e))
|
|
2180
3100
|
|
|
3101
|
+
@app.patch(
|
|
3102
|
+
"/v1/default/banks/{bank_id}",
|
|
3103
|
+
response_model=BankProfileResponse,
|
|
3104
|
+
summary="Partial update memory bank",
|
|
3105
|
+
description="Partially update an agent's profile. Only provided fields will be updated.",
|
|
3106
|
+
operation_id="update_bank",
|
|
3107
|
+
tags=["Banks"],
|
|
3108
|
+
)
|
|
3109
|
+
async def api_update_bank(
|
|
3110
|
+
bank_id: str, request: CreateBankRequest, request_context: RequestContext = Depends(get_request_context)
|
|
3111
|
+
):
|
|
3112
|
+
"""Partially update an agent's profile (name, mission, disposition)."""
|
|
3113
|
+
try:
|
|
3114
|
+
# Ensure bank exists
|
|
3115
|
+
await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
|
3116
|
+
|
|
3117
|
+
# Update name and/or mission if provided
|
|
3118
|
+
mission_value = request.mission or request.background
|
|
3119
|
+
if request.name is not None or mission_value is not None:
|
|
3120
|
+
await app.state.memory.update_bank(
|
|
3121
|
+
bank_id,
|
|
3122
|
+
name=request.name,
|
|
3123
|
+
mission=mission_value,
|
|
3124
|
+
request_context=request_context,
|
|
3125
|
+
)
|
|
3126
|
+
|
|
3127
|
+
# Update disposition if provided
|
|
3128
|
+
if request.disposition is not None:
|
|
3129
|
+
await app.state.memory.update_bank_disposition(
|
|
3130
|
+
bank_id, request.disposition.model_dump(), request_context=request_context
|
|
3131
|
+
)
|
|
3132
|
+
|
|
3133
|
+
# Get final profile
|
|
3134
|
+
final_profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
|
3135
|
+
disposition_dict = (
|
|
3136
|
+
final_profile["disposition"].model_dump()
|
|
3137
|
+
if hasattr(final_profile["disposition"], "model_dump")
|
|
3138
|
+
else dict(final_profile["disposition"])
|
|
3139
|
+
)
|
|
3140
|
+
mission = final_profile.get("mission") or ""
|
|
3141
|
+
return BankProfileResponse(
|
|
3142
|
+
bank_id=bank_id,
|
|
3143
|
+
name=final_profile["name"],
|
|
3144
|
+
disposition=DispositionTraits(**disposition_dict),
|
|
3145
|
+
mission=mission,
|
|
3146
|
+
background=mission, # Backwards compat
|
|
3147
|
+
)
|
|
3148
|
+
except (AuthenticationError, HTTPException):
|
|
3149
|
+
raise
|
|
3150
|
+
except Exception as e:
|
|
3151
|
+
import traceback
|
|
3152
|
+
|
|
3153
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
3154
|
+
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}: {error_detail}")
|
|
3155
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
3156
|
+
|
|
2181
3157
|
@app.delete(
|
|
2182
3158
|
"/v1/default/banks/{bank_id}",
|
|
2183
3159
|
response_model=DeleteResponse,
|
|
@@ -2207,6 +3183,57 @@ def _register_routes(app: FastAPI):
|
|
|
2207
3183
|
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}: {error_detail}")
|
|
2208
3184
|
raise HTTPException(status_code=500, detail=str(e))
|
|
2209
3185
|
|
|
3186
|
+
@app.delete(
|
|
3187
|
+
"/v1/default/banks/{bank_id}/observations",
|
|
3188
|
+
response_model=DeleteResponse,
|
|
3189
|
+
summary="Clear all observations",
|
|
3190
|
+
description="Delete all observations for a memory bank. This is useful for resetting the consolidated knowledge.",
|
|
3191
|
+
operation_id="clear_observations",
|
|
3192
|
+
tags=["Banks"],
|
|
3193
|
+
)
|
|
3194
|
+
async def api_clear_observations(bank_id: str, request_context: RequestContext = Depends(get_request_context)):
|
|
3195
|
+
"""Clear all observations for a bank."""
|
|
3196
|
+
try:
|
|
3197
|
+
result = await app.state.memory.clear_observations(bank_id, request_context=request_context)
|
|
3198
|
+
return DeleteResponse(
|
|
3199
|
+
success=True,
|
|
3200
|
+
message=f"Cleared {result.get('deleted_count', 0)} observations",
|
|
3201
|
+
deleted_count=result.get("deleted_count", 0),
|
|
3202
|
+
)
|
|
3203
|
+
except (AuthenticationError, HTTPException):
|
|
3204
|
+
raise
|
|
3205
|
+
except Exception as e:
|
|
3206
|
+
import traceback
|
|
3207
|
+
|
|
3208
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
3209
|
+
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/observations: {error_detail}")
|
|
3210
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
3211
|
+
|
|
3212
|
+
@app.post(
|
|
3213
|
+
"/v1/default/banks/{bank_id}/consolidate",
|
|
3214
|
+
response_model=ConsolidationResponse,
|
|
3215
|
+
summary="Trigger consolidation",
|
|
3216
|
+
description="Run memory consolidation to create/update observations from recent memories.",
|
|
3217
|
+
operation_id="trigger_consolidation",
|
|
3218
|
+
tags=["Banks"],
|
|
3219
|
+
)
|
|
3220
|
+
async def api_trigger_consolidation(bank_id: str, request_context: RequestContext = Depends(get_request_context)):
|
|
3221
|
+
"""Trigger consolidation for a bank (async)."""
|
|
3222
|
+
try:
|
|
3223
|
+
result = await app.state.memory.submit_async_consolidation(bank_id=bank_id, request_context=request_context)
|
|
3224
|
+
return ConsolidationResponse(
|
|
3225
|
+
operation_id=result["operation_id"],
|
|
3226
|
+
deduplicated=result.get("deduplicated", False),
|
|
3227
|
+
)
|
|
3228
|
+
except (AuthenticationError, HTTPException):
|
|
3229
|
+
raise
|
|
3230
|
+
except Exception as e:
|
|
3231
|
+
import traceback
|
|
3232
|
+
|
|
3233
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
3234
|
+
logger.error(f"Error in POST /v1/default/banks/{bank_id}/consolidate: {error_detail}")
|
|
3235
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
3236
|
+
|
|
2210
3237
|
@app.post(
|
|
2211
3238
|
"/v1/default/banks/{bank_id}/memories",
|
|
2212
3239
|
response_model=RetainResponse,
|