-
Notifications
You must be signed in to change notification settings - Fork 803
Expand file tree
/
Copy pathhttp.py
More file actions
4868 lines (4269 loc) · 198 KB
/
http.py
File metadata and controls
4868 lines (4269 loc) · 198 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
FastAPI application factory and API routes for memory system.
This module provides the create_app function to create and configure
the FastAPI application with all API endpoints.
"""
import asyncio
import json
import logging
import uuid
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from typing import Any, Literal
from fastapi import Depends, FastAPI, File, Form, Header, HTTPException, Query, UploadFile
from hindsight_api.extensions import AuthenticationError
def _parse_metadata(metadata: Any) -> dict[str, Any]:
"""Parse metadata that may be a dict, JSON string, or None."""
if metadata is None:
return {}
if isinstance(metadata, dict):
return metadata
if isinstance(metadata, str):
try:
return json.loads(metadata)
except json.JSONDecodeError:
return {}
return {}
from typing import Callable
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from hindsight_api import MemoryEngine
def FieldWithDefault(default_factory: Callable, **kwargs) -> Any:
"""
Field wrapper that ensures default_factory values appear in OpenAPI schema.
Pydantic doesn't include default_factory in OpenAPI schemas, causing OpenAPI
Generator to make fields Optional with default=None instead of non-optional
with the correct default value.
This wrapper adds json_schema_extra to include the default in the schema.
"""
# Determine the default value for the schema based on the factory
if default_factory is list:
schema_default = []
elif default_factory is dict:
schema_default = {}
else:
# For custom factories (like IncludeOptions), use empty dict as placeholder
schema_default = {}
# Add or merge json_schema_extra
json_extra = kwargs.pop("json_schema_extra", {})
if isinstance(json_extra, dict):
json_extra["default"] = schema_default
else:
# If json_schema_extra was a function, we can't merge easily
# Fall back to just setting default
json_extra = {"default": schema_default}
return Field(default_factory=default_factory, json_schema_extra=json_extra, **kwargs)
from hindsight_api.config import get_config
from hindsight_api.engine.memory_engine import Budget, _current_schema, _get_tiktoken_encoding, fq_table
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES, MemoryFact, TokenUsage
from hindsight_api.engine.search.tags import TagGroup, TagsMatch
from hindsight_api.extensions import HttpExtension, OperationValidationError, load_extension
from hindsight_api.metrics import create_metrics_collector, get_metrics_collector, initialize_metrics
from hindsight_api.models import RequestContext
logger = logging.getLogger(__name__)
class EntityIncludeOptions(BaseModel):
"""Options for including entity observations in recall results."""
max_tokens: int = Field(default=500, description="Maximum tokens for entity observations")
class ChunkIncludeOptions(BaseModel):
"""Options for including chunks in recall results."""
max_tokens: int = Field(default=8192, description="Maximum tokens for chunks (chunks may be truncated)")
class SourceFactsIncludeOptions(BaseModel):
"""Options for including source facts for observation-type results."""
max_tokens: int = Field(
default=4096, description="Maximum total tokens for source facts across all observations (-1 = unlimited)"
)
max_tokens_per_observation: int = Field(
default=-1, description="Maximum tokens of source facts per observation (-1 = unlimited)"
)
class IncludeOptions(BaseModel):
"""Options for including additional data in recall results."""
entities: EntityIncludeOptions | None = Field(
default=EntityIncludeOptions(),
description="Include entity observations. Set to null to disable entity inclusion.",
)
chunks: ChunkIncludeOptions | None = Field(
default=None, description="Include raw chunks. Set to {} to enable, null to disable (default: disabled)."
)
source_facts: SourceFactsIncludeOptions | None = Field(
default=None,
description="Include source facts for observation-type results. Set to {} to enable, null to disable (default: disabled).",
)
class RecallRequest(BaseModel):
"""Request model for recall endpoint."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"query": "What did Alice say about machine learning?",
"types": ["world", "experience"],
"budget": "mid",
"max_tokens": 4096,
"trace": True,
"query_timestamp": "2023-05-30T23:40:00",
"include": {"entities": {"max_tokens": 500}},
"tags": ["user_a"],
"tags_match": "any",
}
}
)
query: str
types: list[str] | None = Field(
default=None,
description="List of fact types to recall: 'world', 'experience', 'observation'. Defaults to world and experience if not specified.",
)
budget: Budget = Budget.MID
max_tokens: int = 4096
trace: bool = False
query_timestamp: str | None = Field(
default=None, description="ISO format date string (e.g., '2023-05-30T23:40:00')"
)
include: IncludeOptions = FieldWithDefault(
IncludeOptions,
description="Options for including additional data (entities are included by default)",
)
tags: list[str] | None = Field(
default=None,
description="Filter memories by tags. If not specified, all memories are returned.",
)
tags_match: TagsMatch = Field(
default="any",
description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), "
"'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).",
)
tag_groups: list[TagGroup] | None = Field(
default=None,
description="Compound tag filter using boolean groups. Groups in the list are AND-ed. "
"Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.",
)
@field_validator("query")
@classmethod
def validate_query_not_empty(cls, v: str) -> str:
from ..engine.search.retrieval import tokenize_query
if not tokenize_query(v):
raise ValueError("query must contain at least one word character after normalization")
return v
@model_validator(mode="after")
def validate_tags_exclusive(self) -> "RecallRequest":
if self.tags is not None and self.tag_groups is not None:
raise ValueError("'tags' and 'tag_groups' are mutually exclusive. Use 'tag_groups' for compound filtering.")
return self
class RecallResult(BaseModel):
"""Single recall result item."""
model_config = {
"populate_by_name": True,
"json_schema_extra": {
"example": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"text": "Alice works at Google on the AI team",
"type": "world",
"entities": ["Alice", "Google"],
"context": "work info",
"occurred_start": "2024-01-15T10:30:00Z",
"occurred_end": "2024-01-15T10:30:00Z",
"mentioned_at": "2024-01-15T10:30:00Z",
"document_id": "session_abc123",
"metadata": {"source": "slack"},
"chunk_id": "456e7890-e12b-34d5-a678-901234567890",
"tags": ["user_a", "user_b"],
}
},
}
id: str
text: str
type: str | None = None # fact type: world, experience, opinion, observation
entities: list[str] | None = None # Entity names mentioned in this fact
context: str | None = None
occurred_start: str | None = None # ISO format date when the event started
occurred_end: str | None = None # ISO format date when the event ended
mentioned_at: str | None = None # ISO format date when the fact was mentioned
document_id: str | None = None # Document this memory belongs to
metadata: dict[str, str] | None = None # User-defined metadata
chunk_id: str | None = None # Chunk this fact was extracted from
tags: list[str] | None = None # Visibility scope tags
source_fact_ids: list[str] | None = (
None # IDs of source facts (observation type only, when source_facts is enabled)
)
class EntityObservationResponse(BaseModel):
"""An observation about an entity."""
text: str
mentioned_at: str | None = None
class EntityStateResponse(BaseModel):
"""Current mental model of an entity."""
entity_id: str
canonical_name: str
observations: list[EntityObservationResponse]
class EntityListItem(BaseModel):
"""Entity list item with summary."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"canonical_name": "John",
"mention_count": 15,
"first_seen": "2024-01-15T10:30:00Z",
"last_seen": "2024-02-01T14:00:00Z",
}
}
)
id: str
canonical_name: str
mention_count: int
first_seen: str | None = None
last_seen: str | None = None
metadata: dict[str, Any] | None = None
class EntityListResponse(BaseModel):
"""Response model for entity list endpoint."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"canonical_name": "John",
"mention_count": 15,
"first_seen": "2024-01-15T10:30:00Z",
"last_seen": "2024-02-01T14:00:00Z",
}
],
"total": 150,
"limit": 100,
"offset": 0,
}
}
)
items: list[EntityListItem]
total: int
limit: int
offset: int
class EntityDetailResponse(BaseModel):
"""Response model for entity detail endpoint."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"canonical_name": "John",
"mention_count": 15,
"first_seen": "2024-01-15T10:30:00Z",
"last_seen": "2024-02-01T14:00:00Z",
"observations": [{"text": "John works at Google", "mentioned_at": "2024-01-15T10:30:00Z"}],
}
}
)
id: str
canonical_name: str
mention_count: int
first_seen: str | None = None
last_seen: str | None = None
metadata: dict[str, Any] | None = None
observations: list[EntityObservationResponse]
class ChunkData(BaseModel):
"""Chunk data for a single chunk."""
id: str
text: str
chunk_index: int
truncated: bool = Field(default=False, description="Whether the chunk text was truncated due to token limits")
class RecallResponse(BaseModel):
"""Response model for recall endpoints."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"results": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"text": "Alice works at Google on the AI team",
"type": "world",
"entities": ["Alice", "Google"],
"context": "work info",
"occurred_start": "2024-01-15T10:30:00Z",
"occurred_end": "2024-01-15T10:30:00Z",
"chunk_id": "456e7890-e12b-34d5-a678-901234567890",
}
],
"trace": {
"query": "What did Alice say about machine learning?",
"num_results": 1,
"time_seconds": 0.123,
},
"entities": {
"Alice": {
"entity_id": "123e4567-e89b-12d3-a456-426614174001",
"canonical_name": "Alice",
"observations": [
{"text": "Alice works at Google on the AI team", "mentioned_at": "2024-01-15T10:30:00Z"}
],
}
},
"chunks": {
"456e7890-e12b-34d5-a678-901234567890": {
"id": "456e7890-e12b-34d5-a678-901234567890",
"text": "Alice works at Google on the AI team. She's been there for 3 years...",
"chunk_index": 0,
}
},
}
}
)
results: list[RecallResult]
trace: dict[str, Any] | None = None
entities: dict[str, EntityStateResponse] | None = Field(
default=None, description="Entity states for entities mentioned in results"
)
chunks: dict[str, ChunkData] | None = Field(default=None, description="Chunks for facts, keyed by chunk_id")
source_facts: dict[str, RecallResult] | None = Field(
default=None, description="Source facts for observation-type results, keyed by fact ID"
)
class EntityInput(BaseModel):
"""Entity to associate with retained content."""
text: str = Field(description="The entity name/text")
type: str | None = Field(default=None, description="Optional entity type (e.g., 'PERSON', 'ORG', 'CONCEPT')")
class MemoryItem(BaseModel):
"""Single memory item for retain."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"content": "Alice mentioned she's working on a new ML model",
"timestamp": "2024-01-15T10:30:00Z",
"context": "team meeting",
"metadata": {"source": "slack", "channel": "engineering"},
"document_id": "meeting_notes_2024_01_15",
"entities": [{"text": "Alice"}, {"text": "ML model", "type": "CONCEPT"}],
"tags": ["user_a", "user_b"],
}
},
)
content: str
timestamp: datetime | str | None = Field(
default=None,
description=(
"When the content occurred. "
"Accepts an ISO 8601 datetime string (e.g. '2024-01-15T10:30:00Z'), null/omitted (defaults to now), "
"or the special string 'unset' to explicitly store without any timestamp "
"(use this for timeless content such as fictional documents or static reference material)."
),
)
context: str | None = None
metadata: dict[str, str] | None = None
document_id: str | None = Field(default=None, description="Optional document ID for this memory item.")
entities: list[EntityInput] | None = Field(
default=None,
description="Optional entities to combine with auto-extracted entities.",
)
tags: list[str] | None = Field(
default=None,
description="Optional tags for visibility scoping. Memories with tags can be filtered during recall.",
)
observation_scopes: Literal["per_tag", "combined", "all_combinations"] | list[list[str]] | None = Field(
default=None,
title="ObservationScopes",
description=(
"How to scope observations during consolidation. "
"'per_tag' runs one consolidation pass per individual tag, creating separate observations for each tag. "
"'combined' (default) runs a single pass with all tags together. "
"A list of tag lists runs one pass per inner list, giving full control over which combinations to use."
),
)
strategy: str | None = Field(
default=None,
description="Named retain strategy for this item. Overrides the bank's default strategy for this item only. "
"Strategies are defined in the bank config under 'retain_strategies'.",
)
@field_validator("timestamp", mode="before")
@classmethod
def validate_timestamp(cls, v):
if v is None or v == "":
return None
if isinstance(v, datetime):
return v
if isinstance(v, str):
if v.lower() == "unset":
return "unset"
try:
# Try parsing as ISO format
return datetime.fromisoformat(v.replace("Z", "+00:00"))
except ValueError as e:
raise ValueError(
f"Invalid timestamp/event_date format: '{v}'. Expected ISO format like '2024-01-15T10:30:00' or '2024-01-15T10:30:00Z', or the special value 'unset' to store without a timestamp."
) from e
raise ValueError(f"timestamp must be a string or datetime, got {type(v).__name__}")
class RetainRequest(BaseModel):
"""Request model for retain endpoint."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"items": [
{"content": "Alice works at Google", "context": "work", "document_id": "conversation_123"},
{
"content": "Bob went hiking yesterday",
"timestamp": "2024-01-15T10:00:00Z",
"document_id": "conversation_123",
},
],
"async": False,
}
}
)
items: list[MemoryItem]
async_: bool = Field(
default=False,
alias="async",
description="If true, process asynchronously in background. If false, wait for completion (default: false)",
)
document_tags: list[str] | None = Field(
default=None,
description="Deprecated. Use item-level tags instead.",
deprecated=True,
)
class FileRetainMetadata(BaseModel):
"""Metadata for a single file in file retain request."""
document_id: str | None = Field(default=None, description="Document ID (auto-generated if not provided)")
context: str | None = Field(default=None, description="Context for the file")
metadata: dict[str, Any] | None = Field(default=None, description="Additional metadata")
tags: list[str] | None = Field(default=None, description="Tags for this file")
timestamp: str | None = Field(default=None, description="ISO timestamp")
parser: str | list[str] | None = Field(
default=None,
description="Parser or ordered fallback chain for this file (overrides request-level parser). "
"E.g. 'iris' or ['iris', 'markitdown'].",
)
strategy: str | None = Field(
default=None,
description="Named retain strategy for this file. Overrides the bank's default strategy. "
"Strategies are defined in the bank config under 'retain_strategies'.",
)
class FileRetainRequest(BaseModel):
"""Request model for file retain endpoint."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"parser": "iris",
"files_metadata": [
{"document_id": "report_2024", "tags": ["quarterly"]},
{"context": "meeting notes", "parser": ["iris", "markitdown"]},
],
}
}
)
parser: str | list[str] | None = Field(
default=None,
description="Default parser or ordered fallback chain for all files in this request. "
"E.g. 'markitdown' or ['iris', 'markitdown']. Falls back to server default if not set. "
"Per-file 'parser' in files_metadata takes precedence over this value.",
)
files_metadata: list[FileRetainMetadata] | None = Field(
default=None,
description="Metadata for each file (optional, must match number of files if provided)",
)
class RetainResponse(BaseModel):
"""Response model for retain endpoint."""
model_config = ConfigDict(
populate_by_name=True,
json_schema_extra={
"example": {
"success": True,
"bank_id": "user123",
"items_count": 2,
"async": False,
"usage": {"input_tokens": 500, "output_tokens": 100, "total_tokens": 600},
}
},
)
success: bool
bank_id: str
items_count: int
is_async: bool = Field(
alias="async", serialization_alias="async", description="Whether the operation was processed asynchronously"
)
operation_id: str | None = Field(
default=None,
description="Operation ID for tracking async operations. Use GET /v1/default/banks/{bank_id}/operations to list operations. Only present when async=true. When items use different per-item strategies, use operation_ids instead.",
)
operation_ids: list[str] | None = Field(
default=None,
description="Operation IDs when items were submitted as multiple strategy groups (async=true with mixed per-item strategies). operation_id is set to the first entry for backward compatibility.",
)
usage: TokenUsage | None = Field(
default=None,
description="Token usage metrics for LLM calls during fact extraction (only present for synchronous operations)",
)
class FileRetainResponse(BaseModel):
"""Response model for file upload endpoint."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"operation_ids": [
"550e8400-e29b-41d4-a716-446655440000",
"550e8400-e29b-41d4-a716-446655440001",
"550e8400-e29b-41d4-a716-446655440002",
],
}
},
)
operation_ids: list[str] = Field(
description="Operation IDs for tracking file conversion operations. Use GET /v1/default/banks/{bank_id}/operations to list operations."
)
class FactsIncludeOptions(BaseModel):
"""Options for including facts (based_on) in reflect results."""
pass # No additional options needed, just enable/disable
class ToolCallsIncludeOptions(BaseModel):
"""Options for including tool calls in reflect results."""
output: bool = Field(
default=True,
description="Include tool outputs in the trace. Set to false to only include inputs (smaller payload).",
)
class ReflectIncludeOptions(BaseModel):
"""Options for including additional data in reflect results."""
facts: FactsIncludeOptions | None = Field(
default=None,
description="Include facts that the answer is based on. Set to {} to enable, null to disable (default: disabled).",
)
tool_calls: ToolCallsIncludeOptions | None = Field(
default=None,
description="Include tool calls trace. Set to {} for full trace (input+output), {output: false} for inputs only.",
)
class ReflectRequest(BaseModel):
"""Request model for reflect endpoint."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"query": "What do you think about artificial intelligence?",
"budget": "low",
"max_tokens": 4096,
"include": {"facts": {}},
"response_schema": {
"type": "object",
"properties": {
"summary": {"type": "string"},
"key_points": {"type": "array", "items": {"type": "string"}},
},
"required": ["summary", "key_points"],
},
"tags": ["user_a"],
"tags_match": "any",
}
}
)
query: str
budget: Budget = Budget.LOW
context: str | None = Field(
default=None,
description="DEPRECATED: Additional context is now concatenated with the query. "
"Pass context directly in the query field instead. "
"If provided, it will be appended to the query for backward compatibility.",
deprecated=True,
)
max_tokens: int = Field(default=4096, description="Maximum tokens for the response")
include: ReflectIncludeOptions = Field(
default_factory=ReflectIncludeOptions, description="Options for including additional data (disabled by default)"
)
response_schema: dict | None = Field(
default=None,
description="Optional JSON Schema for structured output. When provided, the response will include a 'structured_output' field with the LLM response parsed according to this schema.",
)
tags: list[str] | None = Field(
default=None,
description="Filter memories by tags during reflection. If not specified, all memories are considered.",
)
tags_match: TagsMatch = Field(
default="any",
description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), "
"'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).",
)
tag_groups: list[TagGroup] | None = Field(
default=None,
description="Compound tag filter using boolean groups. Groups in the list are AND-ed. "
"Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.",
)
fact_types: list[Literal["world", "experience", "observation"]] | None = Field(
default=None,
description="Filter which fact types are retrieved during reflect. None means all types (world, experience, observation).",
)
exclude_mental_models: bool = Field(
default=False,
description="If true, exclude all mental models from the reflect loop (skip search_mental_models tool).",
)
exclude_mental_model_ids: list[str] | None = Field(
default=None,
description="Exclude specific mental models by ID from the reflect loop.",
)
@field_validator("fact_types")
@classmethod
def validate_reflect_fact_types(cls, v: list[str] | None) -> list[str] | None:
if v is not None and len(v) == 0:
raise ValueError("fact_types must not be empty. Use null to include all fact types.")
return v
@model_validator(mode="after")
def validate_tags_exclusive(self) -> "ReflectRequest":
if self.tags is not None and self.tag_groups is not None:
raise ValueError("'tags' and 'tag_groups' are mutually exclusive. Use 'tag_groups' for compound filtering.")
return self
class ReflectFact(BaseModel):
"""A fact used in think response."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"text": "AI is used in healthcare",
"type": "world",
"context": "healthcare discussion",
"occurred_start": "2024-01-15T10:30:00Z",
"occurred_end": "2024-01-15T10:30:00Z",
}
}
)
id: str | None = None
text: str = Field(
description="Fact text. When type='observation', this contains markdown-formatted consolidated knowledge"
)
type: str | None = None # fact type: world, experience, observation
context: str | None = None
occurred_start: str | None = None
occurred_end: str | None = None
class ReflectDirective(BaseModel):
"""A directive applied during reflect."""
id: str = Field(description="Directive ID")
name: str = Field(description="Directive name")
content: str = Field(description="Directive content")
class ReflectMentalModel(BaseModel):
"""A mental model used during reflect."""
id: str = Field(description="Mental model ID")
text: str = Field(description="Mental model content")
context: str | None = Field(default=None, description="Additional context")
class ReflectToolCall(BaseModel):
"""A tool call made during reflect agent execution."""
tool: str = Field(description="Tool name: lookup, recall, learn, expand")
input: dict = Field(description="Tool input parameters")
output: dict | None = Field(
default=None, description="Tool output (only included when include.tool_calls.output is true)"
)
duration_ms: int = Field(description="Execution time in milliseconds")
iteration: int = Field(default=0, description="Iteration number (1-based) when this tool was called")
class ReflectLLMCall(BaseModel):
"""An LLM call made during reflect agent execution."""
scope: str = Field(description="Call scope: agent_1, agent_2, final, etc.")
duration_ms: int = Field(description="Execution time in milliseconds")
class ReflectBasedOn(BaseModel):
"""Evidence the response is based on: memories, mental models, and directives."""
memories: list[ReflectFact] = FieldWithDefault(list, description="Memory facts used to generate the response")
mental_models: list[ReflectMentalModel] = FieldWithDefault(list, description="Mental models used during reflection")
directives: list[ReflectDirective] = FieldWithDefault(list, description="Directives applied during reflection")
class ReflectTrace(BaseModel):
"""Execution trace of LLM and tool calls during reflection."""
tool_calls: list[ReflectToolCall] = FieldWithDefault(list, description="Tool calls made during reflection")
llm_calls: list[ReflectLLMCall] = FieldWithDefault(list, description="LLM calls made during reflection")
class ReflectResponse(BaseModel):
"""Response model for think endpoint."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"text": "## AI Overview\n\nBased on my understanding, AI is a **transformative technology**:\n\n- Used extensively in healthcare\n- Discussed in recent conversations\n- Continues to evolve rapidly",
"based_on": {
"memories": [
{"id": "123", "text": "AI is used in healthcare", "type": "world"},
{"id": "456", "text": "I discussed AI applications last week", "type": "experience"},
],
},
"structured_output": {
"summary": "AI is transformative",
"key_points": ["Used in healthcare", "Discussed recently"],
},
"usage": {"input_tokens": 1500, "output_tokens": 500, "total_tokens": 2000},
"trace": {
"tool_calls": [{"tool": "recall", "input": {"query": "AI"}, "duration_ms": 150}],
"llm_calls": [{"scope": "agent_1", "duration_ms": 1200}],
"observations": [
{
"id": "obs-1",
"name": "AI Technology",
"type": "concept",
"subtype": "structural",
}
],
},
}
}
)
text: str = Field(
description="The reflect response as well-formatted markdown (headers, lists, bold/italic, code blocks, etc.)"
)
based_on: ReflectBasedOn | None = Field(
default=None,
description="Evidence used to generate the response. Only present when include.facts is set.",
)
structured_output: dict | None = Field(
default=None,
description="Structured output parsed according to the request's response_schema. Only present when response_schema was provided in the request.",
)
usage: TokenUsage | None = Field(
default=None,
description="Token usage metrics for LLM calls during reflection.",
)
trace: ReflectTrace | None = Field(
default=None,
description="Execution trace of tool and LLM calls. Only present when include.tool_calls is set.",
)
class DispositionTraits(BaseModel):
"""Disposition traits that influence how memories are formed and interpreted."""
model_config = ConfigDict(json_schema_extra={"example": {"skepticism": 3, "literalism": 3, "empathy": 3}})
skepticism: int = Field(ge=1, le=5, description="How skeptical vs trusting (1=trusting, 5=skeptical)")
literalism: int = Field(ge=1, le=5, description="How literally to interpret information (1=flexible, 5=literal)")
empathy: int = Field(ge=1, le=5, description="How much to consider emotional context (1=detached, 5=empathetic)")
class BankProfileResponse(BaseModel):
"""Response model for bank profile."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"bank_id": "user123",
"name": "Alice",
"disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
"mission": "I am a software engineer helping my team stay organized and ship quality code",
}
}
)
bank_id: str
name: str
disposition: DispositionTraits
mission: str = Field(description="The agent's mission - who they are and what they're trying to accomplish")
# Deprecated: use mission instead. Kept for backwards compatibility.
background: str | None = Field(default=None, description="Deprecated: use mission instead")
class UpdateDispositionRequest(BaseModel):
"""Request model for updating disposition traits."""
disposition: DispositionTraits
class SetMissionRequest(BaseModel):
"""Request model for setting/updating the agent's mission."""
model_config = ConfigDict(
json_schema_extra={"example": {"content": "I am a PM helping my engineering team stay organized"}}
)
content: str = Field(description="The mission content - who you are and what you're trying to accomplish")
class MissionResponse(BaseModel):
"""Response model for mission update."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"mission": "I am a PM helping my engineering team stay organized and ship quality code.",
}
}
)
mission: str
class AddBackgroundRequest(BaseModel):
"""Request model for adding/merging background information. Deprecated: use SetMissionRequest instead."""
model_config = ConfigDict(
json_schema_extra={"example": {"content": "I was born in Texas", "update_disposition": True}}
)
content: str = Field(description="New background information to add or merge")
update_disposition: bool = Field(
default=True, description="Deprecated - disposition is no longer auto-inferred from mission"
)
class BackgroundResponse(BaseModel):
"""Response model for background update. Deprecated: use MissionResponse instead."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"mission": "I was born in Texas. I am a software engineer with 10 years of experience.",
}
}
)
mission: str
# Deprecated fields kept for backwards compatibility
background: str | None = Field(default=None, description="Deprecated: same as mission")
disposition: DispositionTraits | None = None
class BankListItem(BaseModel):
"""Bank list item with profile summary."""
bank_id: str
name: str | None = None
disposition: DispositionTraits
mission: str | None = None
created_at: str | None = None
updated_at: str | None = None
class BankListResponse(BaseModel):
"""Response model for listing all banks."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"banks": [
{
"bank_id": "user123",
"name": "Alice",
"disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
"mission": "I am a software engineer helping my team ship quality code",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-16T14:20:00Z",
}
]
}
}
)
banks: list[BankListItem]
class CreateBankRequest(BaseModel):
"""Request model for creating/updating a bank."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"retain_mission": "Always include technical decisions and architectural trade-offs. Ignore meeting logistics.",
"observations_mission": "Observations are stable facts about people and projects. Always include preferences and skills.",
}
}
)
# Deprecated fields — kept for backwards compatibility only
name: str | None = Field(default=None, description="Deprecated: display label only, not advertised")
disposition: DispositionTraits | None = Field(
default=None, description="Deprecated: use update_bank_config instead"
)
disposition_skepticism: int | None = Field(
default=None, ge=1, le=5, description="Deprecated: use update_bank_config instead"
)
disposition_literalism: int | None = Field(
default=None, ge=1, le=5, description="Deprecated: use update_bank_config instead"
)
disposition_empathy: int | None = Field(
default=None, ge=1, le=5, description="Deprecated: use update_bank_config instead"
)
# Deprecated: use update_bank_config with reflect_mission instead
mission: str | None = Field(
default=None, description="Deprecated: use update_bank_config with reflect_mission instead"
)
# Deprecated alias for mission
background: str | None = Field(
default=None, description="Deprecated: use update_bank_config with reflect_mission instead"
)
# Reflect configuration