Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b4c73cc24 | ||
|
|
4d5a559a3c | ||
|
|
568e3c3028 | ||
|
|
cbaec36f66 | ||
|
|
d8aada7b0e | ||
|
|
16e1cc4934 | ||
|
|
3ee9437020 | ||
|
|
9671786faf | ||
|
|
33645e08cd | ||
|
|
2f5844b38d | ||
|
|
1267e61edd | ||
|
|
931f2a77ff | ||
|
|
eeff5001af | ||
|
|
f835c731fe | ||
|
|
b6dbd614fc | ||
|
|
32fc9b7477 | ||
|
|
e4f54a6071 | ||
|
|
343b972a95 | ||
|
|
58c02feef0 | ||
|
|
a5f8b58ab5 | ||
|
|
78008a1ad0 | ||
|
|
2128c02e0e | ||
|
|
2eab07834a | ||
|
|
9f41d98172 | ||
|
|
84bab9c5b7 | ||
|
|
d73e552189 | ||
|
|
712a862841 | ||
|
|
f64c5d2097 | ||
|
|
d4bf740618 | ||
|
|
70a7411659 | ||
|
|
dee581396b | ||
|
|
6a1d5fcd30 | ||
|
|
16ed93b9a2 | ||
|
|
581bbf3fc6 | ||
|
|
d00c843262 | ||
|
|
33442f1961 | ||
|
|
a525df4837 | ||
|
|
90a2201655 | ||
|
|
34365c3248 | ||
|
|
06c912df34 | ||
|
|
43dc50dd3f | ||
|
|
7d5d5b2781 | ||
|
|
b79ab2b752 | ||
|
|
cf9918891b | ||
|
|
9372462e13 | ||
|
|
f2fc8f9f26 | ||
|
|
6a80ecbf65 | ||
|
|
870bf4a3d1 | ||
|
|
099f4c925a | ||
|
|
e08faadc17 | ||
|
|
dbd1d1a743 | ||
|
|
c084765950 | ||
|
|
d6ad53986a | ||
|
|
6076354a9c | ||
|
|
3960764522 |
@@ -2547,6 +2547,9 @@ jobs:
|
||||
- name: Run generate-openapi
|
||||
run: ./scripts/generate-openapi.sh
|
||||
|
||||
- name: Run generate-bank-template-schema
|
||||
run: ./scripts/generate-bank-template-schema.sh
|
||||
|
||||
- name: Run generate-clients
|
||||
run: ./scripts/generate-clients.sh
|
||||
|
||||
@@ -2566,6 +2569,7 @@ jobs:
|
||||
echo ""
|
||||
echo "Please run the following commands locally and commit the changes:"
|
||||
echo " ./scripts/generate-openapi.sh"
|
||||
echo " ./scripts/generate-bank-template-schema.sh"
|
||||
echo " ./scripts/generate-clients.sh"
|
||||
echo " ./scripts/generate-docs-skill.sh"
|
||||
echo " ./scripts/hooks/lint.sh"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: hindsight
|
||||
description: Hindsight helm chart
|
||||
type: application
|
||||
version: 0.5.1
|
||||
appVersion: "0.5.1"
|
||||
version: 0.5.2
|
||||
appVersion: "0.5.2"
|
||||
keywords:
|
||||
- ai
|
||||
- memory
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vectorize-io/hindsight-all",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"description": "Node.js programmatic lifecycle manager for Hindsight — embeds a local hindsight daemon in a Node application. Pair with @vectorize-io/hindsight-client for memory operations.",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hindsight-all-slim"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
description = "Hindsight: Agent Memory That Works Like Human Memory - Slim All-in-One Bundle"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-all"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
description = "Hindsight: Agent Memory That Works Like Human Memory - All-in-One Bundle"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -46,4 +46,4 @@ __all__ = [
|
||||
"RemoteTEICrossEncoder",
|
||||
"LLMConfig",
|
||||
]
|
||||
__version__ = "0.5.1"
|
||||
__version__ = "0.5.2"
|
||||
|
||||
@@ -294,6 +294,44 @@ class EntityListResponse(BaseModel):
|
||||
offset: int
|
||||
|
||||
|
||||
class EntityGraphResponse(BaseModel):
|
||||
"""Response model for entity co-occurrence graph endpoint."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"nodes": [
|
||||
{"data": {"id": "uuid-1", "label": "Alice", "mentionCount": 12, "color": "#42a5f5"}},
|
||||
{"data": {"id": "uuid-2", "label": "Google", "mentionCount": 8, "color": "#42a5f5"}},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"data": {
|
||||
"id": "uuid-1-uuid-2",
|
||||
"source": "uuid-1",
|
||||
"target": "uuid-2",
|
||||
"linkType": "cooccurrence",
|
||||
"weight": 5,
|
||||
"color": "#ffd700",
|
||||
"lineStyle": "solid",
|
||||
"lastCooccurred": "2024-02-01T14:00:00Z",
|
||||
}
|
||||
}
|
||||
],
|
||||
"total_entities": 2,
|
||||
"total_edges": 1,
|
||||
"limit": 1000,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
nodes: list[dict[str, Any]]
|
||||
edges: list[dict[str, Any]]
|
||||
total_entities: int
|
||||
total_edges: int
|
||||
limit: int
|
||||
|
||||
|
||||
class EntityDetailResponse(BaseModel):
|
||||
"""Response model for entity detail endpoint."""
|
||||
|
||||
@@ -1431,12 +1469,37 @@ class BankStatsResponse(BaseModel):
|
||||
links_breakdown: dict[str, dict[str, int]]
|
||||
pending_operations: int
|
||||
failed_operations: int
|
||||
operations_by_status: dict[str, int] = Field(
|
||||
default_factory=dict,
|
||||
description="Async operations grouped by status (pending, in_progress, completed, failed, cancelled).",
|
||||
)
|
||||
# Consolidation stats
|
||||
last_consolidated_at: str | None = Field(default=None, description="When consolidation last ran (ISO format)")
|
||||
pending_consolidation: int = Field(default=0, description="Number of memories not yet processed into observations")
|
||||
total_observations: int = Field(default=0, description="Total number of observations")
|
||||
|
||||
|
||||
class MemoryTimeseriesBucket(BaseModel):
|
||||
"""One bucket in the memory ingestion time-series."""
|
||||
|
||||
time: str = Field(description="Bucket start timestamp in ISO-8601 (UTC).")
|
||||
world: int = Field(default=0, description="World-fact memories ingested in this bucket.")
|
||||
experience: int = Field(default=0, description="Experience memories ingested in this bucket.")
|
||||
observation: int = Field(default=0, description="Observations recorded in this bucket.")
|
||||
|
||||
|
||||
class MemoriesTimeseriesResponse(BaseModel):
|
||||
"""Time-series of memory ingestion bucketed by time and fact type."""
|
||||
|
||||
bank_id: str
|
||||
period: str = Field(description="One of: 1h, 12h, 1d, 7d, 30d, 90d.")
|
||||
trunc: str = Field(description="Bucket granularity: minute, hour, day.")
|
||||
buckets: list[MemoryTimeseriesBucket] = Field(
|
||||
default_factory=list,
|
||||
description="Per-bucket counts, always returned fully padded for the requested period.",
|
||||
)
|
||||
|
||||
|
||||
# Mental Model models
|
||||
|
||||
|
||||
@@ -1526,6 +1589,27 @@ class MentalModelTrigger(BaseModel):
|
||||
"Supports nested and/or/not expressions for complex tag-based scoping."
|
||||
),
|
||||
)
|
||||
include_chunks: bool | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Override whether the internal recall used during refresh returns raw chunk text. "
|
||||
"None means use the bank/global config default (recall_include_chunks)."
|
||||
),
|
||||
)
|
||||
recall_max_tokens: int | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Override the token budget for facts returned by the internal recall during refresh. "
|
||||
"None means use the bank/global config default (recall_max_tokens)."
|
||||
),
|
||||
)
|
||||
recall_chunks_max_tokens: int | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Override the token budget for raw chunks returned by the internal recall during refresh. "
|
||||
"None means use the bank/global config default (recall_chunks_max_tokens)."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("fact_types")
|
||||
@classmethod
|
||||
@@ -1673,6 +1757,36 @@ class BankTemplateConfig(BaseModel):
|
||||
entities_allow_free_form: bool | None = Field(
|
||||
default=None, description="Allow entities outside the label vocabulary"
|
||||
)
|
||||
retain_default_strategy: str | None = Field(
|
||||
default=None, description="Name of the default retain strategy (key into retain_strategies map)"
|
||||
)
|
||||
retain_strategies: dict | None = Field(
|
||||
default=None, description="Map of retain strategy name to per-strategy config dict"
|
||||
)
|
||||
retain_chunk_batch_size: int | None = Field(
|
||||
default=None, description="Max chunks per streaming batch (0 disables batching)"
|
||||
)
|
||||
mcp_enabled_tools: list[str] | None = Field(
|
||||
default=None, description="MCP tool allowlist for this bank (None = all tools)"
|
||||
)
|
||||
consolidation_llm_batch_size: int | None = Field(
|
||||
default=None, description="LLM batch size for observation consolidation"
|
||||
)
|
||||
consolidation_source_facts_max_tokens: int | None = Field(
|
||||
default=None, description="Max tokens of source facts per consolidation batch"
|
||||
)
|
||||
consolidation_source_facts_max_tokens_per_observation: int | None = Field(
|
||||
default=None, description="Max tokens of source facts per observation"
|
||||
)
|
||||
max_observations_per_scope: int | None = Field(
|
||||
default=None, description="Max observations to retain per consolidation scope"
|
||||
)
|
||||
reflect_source_facts_max_tokens: int | None = Field(
|
||||
default=None, description="Max tokens of source facts per reflect call"
|
||||
)
|
||||
llm_gemini_safety_settings: list | None = Field(
|
||||
default=None, description="Per-bank Gemini/VertexAI safety filter settings"
|
||||
)
|
||||
|
||||
def get_config_updates(self) -> dict[str, Any]:
|
||||
"""Return only the fields that were explicitly set (non-None)."""
|
||||
@@ -2084,6 +2198,10 @@ class OperationStatusResponse(BaseModel):
|
||||
child_operations: list[ChildOperationStatus] | None = Field(
|
||||
default=None, description="Child operations for batch operations (if applicable)"
|
||||
)
|
||||
task_payload: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
description="Raw task payload (params the operation was submitted with). Only populated when include_payload=true.",
|
||||
)
|
||||
|
||||
|
||||
class AsyncOperationSubmitResponse(BaseModel):
|
||||
@@ -3248,6 +3366,7 @@ def _register_routes(app: FastAPI):
|
||||
links_breakdown=links_breakdown,
|
||||
pending_operations=ops.get("pending", 0),
|
||||
failed_operations=ops.get("failed", 0),
|
||||
operations_by_status=ops,
|
||||
last_consolidated_at=stats["last_consolidated_at"],
|
||||
pending_consolidation=stats["pending_consolidation"],
|
||||
total_observations=stats["total_observations"],
|
||||
@@ -3263,6 +3382,35 @@ def _register_routes(app: FastAPI):
|
||||
logger.error(f"Error in /v1/default/banks/{bank_id}/stats: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/stats/memories-timeseries",
|
||||
response_model=MemoriesTimeseriesResponse,
|
||||
summary="Memory ingestion time-series",
|
||||
description="Memories ingested over a period, bucketed by time and broken down by fact type.",
|
||||
operation_id="get_memories_timeseries",
|
||||
tags=["Banks"],
|
||||
)
|
||||
async def api_memories_timeseries(
|
||||
bank_id: str,
|
||||
period: str = "7d",
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
try:
|
||||
data = await app.state.memory.get_memories_timeseries(
|
||||
bank_id, period=period, request_context=request_context
|
||||
)
|
||||
return MemoriesTimeseriesResponse(**data)
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in /v1/default/banks/{bank_id}/stats/memories-timeseries: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/entities",
|
||||
response_model=EntityListResponse,
|
||||
@@ -3299,6 +3447,36 @@ def _register_routes(app: FastAPI):
|
||||
logger.error(f"Error in /v1/default/banks/{bank_id}/entities: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/entities/graph",
|
||||
response_model=EntityGraphResponse,
|
||||
summary="Get entity co-occurrence graph",
|
||||
description="Return a graph of entities (nodes) and their co-occurrences (edges) for visualization.",
|
||||
operation_id="get_entity_graph",
|
||||
tags=["Entities"],
|
||||
)
|
||||
async def api_entity_graph(
|
||||
bank_id: str,
|
||||
limit: int = Query(default=1000, description="Maximum number of co-occurrence edges to return"),
|
||||
min_count: int = Query(default=1, description="Minimum cooccurrence_count to include an edge"),
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Return entity co-occurrence graph for a bank."""
|
||||
try:
|
||||
return await app.state.memory.get_entity_graph(
|
||||
bank_id, limit=limit, min_count=min_count, request_context=request_context
|
||||
)
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in /v1/default/banks/{bank_id}/entities/graph: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/entities/{entity_id}",
|
||||
response_model=EntityDetailResponse,
|
||||
@@ -4168,7 +4346,13 @@ def _register_routes(app: FastAPI):
|
||||
tags=["Operations"],
|
||||
)
|
||||
async def api_get_operation_status(
|
||||
bank_id: str, operation_id: str, request_context: RequestContext = Depends(get_request_context)
|
||||
bank_id: str,
|
||||
operation_id: str,
|
||||
include_payload: bool = Query(
|
||||
default=False,
|
||||
description="Include the raw task payload (submission params) in the response. May be large.",
|
||||
),
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Get the status of an async operation."""
|
||||
try:
|
||||
@@ -4178,7 +4362,9 @@ def _register_routes(app: FastAPI):
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid operation_id format: {operation_id}")
|
||||
|
||||
result = await app.state.memory.get_operation_status(bank_id, operation_id, request_context=request_context)
|
||||
result = await app.state.memory.get_operation_status(
|
||||
bank_id, operation_id, request_context=request_context, include_payload=include_payload
|
||||
)
|
||||
return OperationStatusResponse(**result)
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
|
||||
@@ -339,6 +339,7 @@ ENV_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS = "HINDSIGHT_API_CONSOLIDATION_SOURCE_
|
||||
ENV_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION = (
|
||||
"HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION"
|
||||
)
|
||||
ENV_CONSOLIDATION_MAX_ATTEMPTS = "HINDSIGHT_API_CONSOLIDATION_MAX_ATTEMPTS"
|
||||
ENV_OBSERVATIONS_MISSION = "HINDSIGHT_API_OBSERVATIONS_MISSION"
|
||||
ENV_MAX_OBSERVATIONS_PER_SCOPE = "HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE"
|
||||
ENV_ENABLE_OBSERVATION_HISTORY = "HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY"
|
||||
@@ -387,6 +388,9 @@ ENV_REFLECT_MAX_CONTEXT_TOKENS = "HINDSIGHT_API_REFLECT_MAX_CONTEXT_TOKENS"
|
||||
ENV_REFLECT_WALL_TIMEOUT = "HINDSIGHT_API_REFLECT_WALL_TIMEOUT"
|
||||
ENV_REFLECT_MISSION = "HINDSIGHT_API_REFLECT_MISSION"
|
||||
ENV_REFLECT_SOURCE_FACTS_MAX_TOKENS = "HINDSIGHT_API_REFLECT_SOURCE_FACTS_MAX_TOKENS"
|
||||
ENV_RECALL_INCLUDE_CHUNKS = "HINDSIGHT_API_RECALL_INCLUDE_CHUNKS"
|
||||
ENV_RECALL_MAX_TOKENS = "HINDSIGHT_API_RECALL_MAX_TOKENS"
|
||||
ENV_RECALL_CHUNKS_MAX_TOKENS = "HINDSIGHT_API_RECALL_CHUNKS_MAX_TOKENS"
|
||||
|
||||
# Audit log settings
|
||||
ENV_AUDIT_LOG_ENABLED = "HINDSIGHT_API_AUDIT_LOG_ENABLED"
|
||||
@@ -551,6 +555,7 @@ DEFAULT_FILE_DELETE_AFTER_RETAIN = True # Delete file bytes after retain (saves
|
||||
DEFAULT_ENABLE_OBSERVATIONS = True # Observations enabled by default
|
||||
DEFAULT_ENABLE_OBSERVATION_HISTORY = True # Observation history tracking enabled by default
|
||||
DEFAULT_ENABLE_MENTAL_MODEL_HISTORY = True # Mental model history tracking enabled by default
|
||||
DEFAULT_CONSOLIDATION_MAX_ATTEMPTS = 3 # Outer retry attempts for consolidation LLM batch calls
|
||||
DEFAULT_CONSOLIDATION_BATCH_SIZE = 50 # Memories to load per batch (internal memory optimization)
|
||||
DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 8 # Facts per LLM call (1 = no batching; >1 = batch mode)
|
||||
DEFAULT_CONSOLIDATION_MAX_TOKENS = 512 # Max tokens for recall when finding related observations
|
||||
@@ -587,6 +592,9 @@ DEFAULT_REFLECT_MAX_ITERATIONS = 10 # Max tool call iterations before forcing r
|
||||
DEFAULT_REFLECT_MAX_CONTEXT_TOKENS = 100_000 # Max accumulated context tokens before forcing final prompt
|
||||
DEFAULT_REFLECT_WALL_TIMEOUT = 300 # Wall-clock timeout in seconds for the entire reflect operation (5 minutes)
|
||||
DEFAULT_REFLECT_SOURCE_FACTS_MAX_TOKENS = -1 # Token budget for source facts in search_observations (-1 = disabled)
|
||||
DEFAULT_RECALL_INCLUDE_CHUNKS = True # Whether internal recall (e.g. mental model refresh) returns raw chunks
|
||||
DEFAULT_RECALL_MAX_TOKENS = 2048 # Token budget for facts returned by internal recall
|
||||
DEFAULT_RECALL_CHUNKS_MAX_TOKENS = 1000 # Token budget for raw chunks returned by internal recall
|
||||
|
||||
# Disposition defaults (None = not set, fall back to bank DB value or 3)
|
||||
DEFAULT_DISPOSITION_SKEPTICISM = None
|
||||
@@ -911,6 +919,7 @@ class HindsightConfig:
|
||||
consolidation_max_tokens: int
|
||||
consolidation_source_facts_max_tokens: int
|
||||
consolidation_source_facts_max_tokens_per_observation: int
|
||||
consolidation_max_attempts: int
|
||||
observations_mission: str | None
|
||||
max_observations_per_scope: int
|
||||
|
||||
@@ -925,6 +934,11 @@ class HindsightConfig:
|
||||
reflect_mission: str | None
|
||||
reflect_source_facts_max_tokens: int
|
||||
|
||||
# Recall settings (used by internal recall, e.g. during mental model refresh)
|
||||
recall_include_chunks: bool
|
||||
recall_max_tokens: int
|
||||
recall_chunks_max_tokens: int
|
||||
|
||||
# Disposition settings (hierarchical - can be overridden per bank; None = fall back to DB)
|
||||
disposition_skepticism: int | None
|
||||
disposition_literalism: int | None
|
||||
@@ -1038,6 +1052,10 @@ class HindsightConfig:
|
||||
# Reflect settings
|
||||
"reflect_mission",
|
||||
"reflect_source_facts_max_tokens",
|
||||
# Recall settings (used by internal recall, e.g. mental model refresh)
|
||||
"recall_include_chunks",
|
||||
"recall_max_tokens",
|
||||
"recall_chunks_max_tokens",
|
||||
# Disposition settings
|
||||
"disposition_skepticism",
|
||||
"disposition_literalism",
|
||||
@@ -1489,6 +1507,9 @@ class HindsightConfig:
|
||||
str(DEFAULT_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION),
|
||||
)
|
||||
),
|
||||
consolidation_max_attempts=int(
|
||||
os.getenv(ENV_CONSOLIDATION_MAX_ATTEMPTS, str(DEFAULT_CONSOLIDATION_MAX_ATTEMPTS))
|
||||
),
|
||||
observations_mission=os.getenv(ENV_OBSERVATIONS_MISSION) or DEFAULT_OBSERVATIONS_MISSION,
|
||||
max_observations_per_scope=int(
|
||||
os.getenv(ENV_MAX_OBSERVATIONS_PER_SCOPE, str(DEFAULT_MAX_OBSERVATIONS_PER_SCOPE))
|
||||
@@ -1523,6 +1544,12 @@ class HindsightConfig:
|
||||
reflect_source_facts_max_tokens=int(
|
||||
os.getenv(ENV_REFLECT_SOURCE_FACTS_MAX_TOKENS, str(DEFAULT_REFLECT_SOURCE_FACTS_MAX_TOKENS))
|
||||
),
|
||||
recall_include_chunks=os.getenv(ENV_RECALL_INCLUDE_CHUNKS, str(DEFAULT_RECALL_INCLUDE_CHUNKS)).lower()
|
||||
in ("true", "1", "yes"),
|
||||
recall_max_tokens=int(os.getenv(ENV_RECALL_MAX_TOKENS, str(DEFAULT_RECALL_MAX_TOKENS))),
|
||||
recall_chunks_max_tokens=int(
|
||||
os.getenv(ENV_RECALL_CHUNKS_MAX_TOKENS, str(DEFAULT_RECALL_CHUNKS_MAX_TOKENS))
|
||||
),
|
||||
# Disposition settings (None = fall back to DB value)
|
||||
disposition_skepticism=int(os.getenv(ENV_DISPOSITION_SKEPTICISM))
|
||||
if os.getenv(ENV_DISPOSITION_SKEPTICISM)
|
||||
|
||||
@@ -1131,11 +1131,13 @@ async def _consolidate_batch_with_llm(
|
||||
memories: list[dict[str, Any]],
|
||||
union_observations: "list[MemoryFact]",
|
||||
union_source_facts: "dict[str, MemoryFact]",
|
||||
config: Any = None,
|
||||
config: Any,
|
||||
remaining_observation_slots: int | None = None,
|
||||
max_observations_per_scope: int = -1,
|
||||
) -> _BatchLLMResult:
|
||||
"""Single LLM call for a batch of facts against a pooled set of observations."""
|
||||
if config is None:
|
||||
raise ValueError("config is required for _consolidate_batch_with_llm")
|
||||
if union_observations:
|
||||
obs_list = _build_observations_for_llm(union_observations, union_source_facts)
|
||||
observations_text = json.dumps(obs_list, indent=2)
|
||||
@@ -1172,8 +1174,7 @@ async def _consolidate_batch_with_llm(
|
||||
f"(out of {max_observations_per_scope}). Prefer UPDATE over CREATE when possible."
|
||||
)
|
||||
|
||||
observations_mission = config.observations_mission if config is not None else None
|
||||
prompt_template = build_batch_consolidation_prompt(observations_mission, observation_capacity_note)
|
||||
prompt_template = build_batch_consolidation_prompt(config.observations_mission, observation_capacity_note)
|
||||
prompt = prompt_template.format(
|
||||
facts_text=facts_lines,
|
||||
observations_text=observations_text,
|
||||
@@ -1182,15 +1183,19 @@ async def _consolidate_batch_with_llm(
|
||||
# Use a constrained response model when observation limit is active
|
||||
response_model = _build_response_model(max_creates=remaining_observation_slots)
|
||||
|
||||
max_attempts = 3
|
||||
max_attempts = config.consolidation_max_attempts
|
||||
inner_max_retries = config.consolidation_llm_max_retries
|
||||
last_exc: Exception | None = None
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
try:
|
||||
response: _ConsolidationBatchResponse = await llm_config.call(
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
response_format=response_model,
|
||||
scope="consolidation",
|
||||
)
|
||||
call_kwargs: dict[str, Any] = {
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"response_format": response_model,
|
||||
"scope": "consolidation",
|
||||
}
|
||||
if inner_max_retries is not None:
|
||||
call_kwargs["max_retries"] = inner_max_retries
|
||||
response: _ConsolidationBatchResponse = await llm_config.call(**call_kwargs)
|
||||
# Defensive truncation: some LLM providers may not enforce JSON schema max_length
|
||||
creates = response.creates
|
||||
if remaining_observation_slots is not None and remaining_observation_slots >= 0:
|
||||
|
||||
@@ -24,7 +24,13 @@ import asyncpg
|
||||
import httpx
|
||||
import tiktoken
|
||||
|
||||
from ..config import DEFAULT_REFLECT_SOURCE_FACTS_MAX_TOKENS, get_config
|
||||
from ..config import (
|
||||
DEFAULT_RECALL_CHUNKS_MAX_TOKENS,
|
||||
DEFAULT_RECALL_INCLUDE_CHUNKS,
|
||||
DEFAULT_RECALL_MAX_TOKENS,
|
||||
DEFAULT_REFLECT_SOURCE_FACTS_MAX_TOKENS,
|
||||
get_config,
|
||||
)
|
||||
from ..metrics import get_metrics_collector
|
||||
from ..tracing import create_operation_span
|
||||
from ..utils import mask_network_location
|
||||
@@ -228,6 +234,44 @@ def _get_tiktoken_encoding():
|
||||
return _TIKTOKEN_ENCODING
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _TimeseriesPeriodConfig:
|
||||
"""How one period slices the time axis for the memories-ingested chart."""
|
||||
|
||||
interval: str # postgres interval literal used in the `now() - interval '...'` filter
|
||||
trunc: str # date_trunc unit (minute/hour/day)
|
||||
step: timedelta # distance between adjacent buckets
|
||||
count: int # total buckets rendered for the period
|
||||
|
||||
|
||||
_MEMORIES_TIMESERIES_PERIODS: dict[str, _TimeseriesPeriodConfig] = {
|
||||
"1h": _TimeseriesPeriodConfig("1 hour", "minute", timedelta(minutes=1), 60),
|
||||
"12h": _TimeseriesPeriodConfig("12 hours", "hour", timedelta(hours=1), 12),
|
||||
"1d": _TimeseriesPeriodConfig("24 hours", "hour", timedelta(hours=1), 24),
|
||||
"7d": _TimeseriesPeriodConfig("7 days", "day", timedelta(days=1), 7),
|
||||
"30d": _TimeseriesPeriodConfig("30 days", "day", timedelta(days=1), 30),
|
||||
"90d": _TimeseriesPeriodConfig("90 days", "day", timedelta(days=1), 90),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryTimeseriesBucketData:
|
||||
"""One bucket of the memories-ingested time series (engine-side)."""
|
||||
|
||||
time: str
|
||||
world: int = 0
|
||||
experience: int = 0
|
||||
observation: int = 0
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"time": self.time,
|
||||
"world": self.world,
|
||||
"experience": self.experience,
|
||||
"observation": self.observation,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RefreshTagFiltering:
|
||||
"""Resolved tag filtering parameters for mental model refresh."""
|
||||
@@ -952,6 +996,9 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
fact_types = trigger_data.get("fact_types")
|
||||
exclude_mental_models = trigger_data.get("exclude_mental_models", False)
|
||||
stored_exclude_ids: list[str] = trigger_data.get("exclude_mental_model_ids") or []
|
||||
recall_include_chunks_override = trigger_data.get("include_chunks")
|
||||
recall_max_tokens_override = trigger_data.get("recall_max_tokens")
|
||||
recall_chunks_max_tokens_override = trigger_data.get("recall_chunks_max_tokens")
|
||||
|
||||
tag_filtering = _resolve_refresh_tag_filtering(mental_model.get("tags"), trigger_data)
|
||||
|
||||
@@ -967,6 +1014,9 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
fact_types=fact_types,
|
||||
exclude_mental_models=exclude_mental_models,
|
||||
exclude_mental_model_ids=list({*stored_exclude_ids, mental_model_id}),
|
||||
recall_include_chunks=recall_include_chunks_override,
|
||||
recall_max_tokens_override=recall_max_tokens_override,
|
||||
recall_chunks_max_tokens_override=recall_chunks_max_tokens_override,
|
||||
)
|
||||
|
||||
generated_content = reflect_result.text or "No content generated"
|
||||
@@ -5399,6 +5449,9 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
exclude_mental_model_ids: list[str] | None = None,
|
||||
fact_types: list[str] | None = None,
|
||||
exclude_mental_models: bool = False,
|
||||
recall_include_chunks: bool | None = None,
|
||||
recall_max_tokens_override: int | None = None,
|
||||
recall_chunks_max_tokens_override: int | None = None,
|
||||
_skip_span: bool = False,
|
||||
) -> ReflectResult:
|
||||
"""
|
||||
@@ -5521,6 +5574,23 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
"reflect_source_facts_max_tokens", DEFAULT_REFLECT_SOURCE_FACTS_MAX_TOKENS
|
||||
)
|
||||
|
||||
# Resolve recall overrides: caller arg (e.g. mental model trigger) → bank config → env default
|
||||
effective_recall_include_chunks = (
|
||||
recall_include_chunks
|
||||
if recall_include_chunks is not None
|
||||
else config_dict.get("recall_include_chunks", DEFAULT_RECALL_INCLUDE_CHUNKS)
|
||||
)
|
||||
effective_recall_max_tokens = (
|
||||
recall_max_tokens_override
|
||||
if recall_max_tokens_override is not None
|
||||
else config_dict.get("recall_max_tokens", DEFAULT_RECALL_MAX_TOKENS)
|
||||
)
|
||||
effective_recall_chunks_max_tokens = (
|
||||
recall_chunks_max_tokens_override
|
||||
if recall_chunks_max_tokens_override is not None
|
||||
else config_dict.get("recall_chunks_max_tokens", DEFAULT_RECALL_CHUNKS_MAX_TOKENS)
|
||||
)
|
||||
|
||||
async def search_observations_fn(q: str, max_tokens: int = 5000) -> dict[str, Any]:
|
||||
return await tool_search_observations(
|
||||
self,
|
||||
@@ -5541,7 +5611,14 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
recall_fact_types = [ft for ft in (fact_types or ["world", "experience"]) if ft in ("world", "experience")]
|
||||
include_recall = bool(recall_fact_types)
|
||||
|
||||
async def recall_fn(q: str, max_tokens: int = 4096, max_chunk_tokens: int = 1000) -> dict[str, Any]:
|
||||
# Defaults are bound at closure-definition time (re-evaluated on each
|
||||
# reflect_async call), so per-bank/per-trigger overrides apply when the
|
||||
# agent invokes recall without explicit token args.
|
||||
async def recall_fn(
|
||||
q: str,
|
||||
max_tokens: int = effective_recall_max_tokens,
|
||||
max_chunk_tokens: int = effective_recall_chunks_max_tokens,
|
||||
) -> dict[str, Any]:
|
||||
return await tool_recall(
|
||||
self,
|
||||
bank_id,
|
||||
@@ -5553,6 +5630,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
tag_groups=tag_groups,
|
||||
max_chunk_tokens=max_chunk_tokens,
|
||||
fact_types=recall_fact_types if fact_types is not None else None,
|
||||
include_chunks=effective_recall_include_chunks,
|
||||
)
|
||||
|
||||
async def expand_fn(memory_ids: list[str], depth: str) -> dict[str, Any]:
|
||||
@@ -5908,6 +5986,108 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
async def get_entity_graph(
|
||||
self,
|
||||
bank_id: str,
|
||||
*,
|
||||
limit: int = 1000,
|
||||
min_count: int = 1,
|
||||
request_context: "RequestContext",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get entity co-occurrence graph for visualization.
|
||||
|
||||
Returns nodes for entities and edges from the materialized
|
||||
entity_cooccurrences table. Edges are ordered by cooccurrence_count DESC
|
||||
and capped at `limit` to keep the payload renderable.
|
||||
"""
|
||||
await self._authenticate_tenant(request_context)
|
||||
if self._operation_validator:
|
||||
from hindsight_api.extensions import BankReadContext
|
||||
|
||||
ctx = BankReadContext(bank_id=bank_id, operation="get_entity_graph", request_context=request_context)
|
||||
await self._validate_operation(self._operation_validator.validate_bank_read(ctx))
|
||||
pool = await self._get_pool()
|
||||
async with acquire_with_retry(pool) as conn:
|
||||
edge_rows = await conn.fetch(
|
||||
f"""
|
||||
SELECT ec.entity_id_1,
|
||||
ec.entity_id_2,
|
||||
ec.cooccurrence_count,
|
||||
ec.last_cooccurred,
|
||||
e1.canonical_name AS name_1,
|
||||
e1.mention_count AS mention_count_1,
|
||||
e2.canonical_name AS name_2,
|
||||
e2.mention_count AS mention_count_2
|
||||
FROM {fq_table("entity_cooccurrences")} ec
|
||||
JOIN {fq_table("entities")} e1 ON e1.id = ec.entity_id_1
|
||||
JOIN {fq_table("entities")} e2 ON e2.id = ec.entity_id_2
|
||||
WHERE e1.bank_id = $1
|
||||
AND e2.bank_id = $1
|
||||
AND ec.cooccurrence_count >= $2
|
||||
ORDER BY ec.cooccurrence_count DESC, ec.last_cooccurred DESC
|
||||
LIMIT $3
|
||||
""",
|
||||
bank_id,
|
||||
min_count,
|
||||
limit,
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class _EntityNode:
|
||||
id: str
|
||||
label: str
|
||||
mention_count: int
|
||||
|
||||
nodes_by_id: dict[str, _EntityNode] = {}
|
||||
edges: list[dict[str, Any]] = []
|
||||
for row in edge_rows:
|
||||
for eid, name, mentions in (
|
||||
(row["entity_id_1"], row["name_1"], row["mention_count_1"]),
|
||||
(row["entity_id_2"], row["name_2"], row["mention_count_2"]),
|
||||
):
|
||||
key = str(eid)
|
||||
if key not in nodes_by_id:
|
||||
nodes_by_id[key] = _EntityNode(id=key, label=name, mention_count=mentions or 0)
|
||||
|
||||
from_id = str(row["entity_id_1"])
|
||||
to_id = str(row["entity_id_2"])
|
||||
count = row["cooccurrence_count"]
|
||||
edges.append(
|
||||
{
|
||||
"data": {
|
||||
"id": f"{from_id}-{to_id}",
|
||||
"source": from_id,
|
||||
"target": to_id,
|
||||
"linkType": "cooccurrence",
|
||||
"weight": count,
|
||||
"color": "#ffd700",
|
||||
"lineStyle": "solid",
|
||||
"lastCooccurred": row["last_cooccurred"].isoformat() if row["last_cooccurred"] else None,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
nodes = [
|
||||
{
|
||||
"data": {
|
||||
"id": n.id,
|
||||
"label": n.label,
|
||||
"mentionCount": n.mention_count,
|
||||
"color": "#42a5f5" if n.mention_count > 1 else "#90caf9",
|
||||
}
|
||||
}
|
||||
for n in nodes_by_id.values()
|
||||
]
|
||||
|
||||
return {
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
"total_entities": len(nodes),
|
||||
"total_edges": len(edges),
|
||||
"limit": limit,
|
||||
}
|
||||
|
||||
async def list_tags(
|
||||
self,
|
||||
bank_id: str,
|
||||
@@ -6123,6 +6303,90 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
"total_observations": node_counts.get("observation", 0),
|
||||
}
|
||||
|
||||
async def get_memories_timeseries(
|
||||
self,
|
||||
bank_id: str,
|
||||
*,
|
||||
period: str,
|
||||
request_context: "RequestContext",
|
||||
) -> dict[str, Any]:
|
||||
"""Memory ingestion bucketed by time, broken down by fact_type.
|
||||
|
||||
Always returns the full expected bucket set for the period so the
|
||||
chart line is continuous (empty buckets show as zeros). Buckets are
|
||||
anchored on UTC boundaries — we do this (rather than the PG session
|
||||
timezone) so the API response is deterministic regardless of where
|
||||
the database is deployed, and so the control-plane chart can match
|
||||
buckets by ISO key on the client side.
|
||||
"""
|
||||
await self._authenticate_tenant(request_context)
|
||||
if self._operation_validator:
|
||||
from hindsight_api.extensions import BankReadContext
|
||||
|
||||
ctx = BankReadContext(bank_id=bank_id, operation="get_memories_timeseries", request_context=request_context)
|
||||
await self._validate_operation(self._operation_validator.validate_bank_read(ctx))
|
||||
|
||||
cfg = _MEMORIES_TIMESERIES_PERIODS.get(period) or _MEMORIES_TIMESERIES_PERIODS["7d"]
|
||||
if period not in _MEMORIES_TIMESERIES_PERIODS:
|
||||
period = "7d"
|
||||
|
||||
pool = await self._get_pool()
|
||||
async with acquire_with_retry(pool) as conn:
|
||||
rows = await conn.fetch(
|
||||
f"""
|
||||
SELECT date_trunc('{cfg.trunc}', created_at AT TIME ZONE 'UTC') AS bucket,
|
||||
fact_type, COUNT(*) AS count
|
||||
FROM {fq_table("memory_units")}
|
||||
WHERE bank_id = $1
|
||||
AND created_at >= now() - interval '{cfg.interval}'
|
||||
GROUP BY bucket, fact_type
|
||||
ORDER BY bucket
|
||||
""",
|
||||
bank_id,
|
||||
)
|
||||
|
||||
# Build the canonical bucket list anchored on the most recent UTC boundary.
|
||||
now_utc = datetime.utcnow()
|
||||
if cfg.trunc == "minute":
|
||||
end = now_utc.replace(second=0, microsecond=0)
|
||||
elif cfg.trunc == "hour":
|
||||
end = now_utc.replace(minute=0, second=0, microsecond=0)
|
||||
else:
|
||||
end = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
buckets: list[MemoryTimeseriesBucketData] = []
|
||||
by_iso: dict[str, MemoryTimeseriesBucketData] = {}
|
||||
for i in range(cfg.count):
|
||||
t = end - cfg.step * (cfg.count - 1 - i)
|
||||
entry = MemoryTimeseriesBucketData(time=t.isoformat())
|
||||
buckets.append(entry)
|
||||
by_iso[entry.time] = entry
|
||||
|
||||
for row in rows:
|
||||
# asyncpg hands us a tz-aware datetime when the column is timestamptz.
|
||||
# Normalize to the naive-UTC format we used for the dict keys.
|
||||
bucket_dt = row["bucket"]
|
||||
if bucket_dt.tzinfo is not None:
|
||||
bucket_dt = bucket_dt.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
entry = by_iso.get(bucket_dt.isoformat())
|
||||
if entry is None:
|
||||
# Row fell outside the requested window (clock skew / edge case).
|
||||
continue
|
||||
ft = row["fact_type"]
|
||||
if ft == "world":
|
||||
entry.world += row["count"]
|
||||
elif ft == "experience":
|
||||
entry.experience += row["count"]
|
||||
elif ft == "observation":
|
||||
entry.observation += row["count"]
|
||||
|
||||
return {
|
||||
"bank_id": bank_id,
|
||||
"period": period,
|
||||
"trunc": cfg.trunc,
|
||||
"buckets": [b.as_dict() for b in buckets],
|
||||
}
|
||||
|
||||
async def get_entity(
|
||||
self,
|
||||
bank_id: str,
|
||||
@@ -6770,12 +7034,15 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
fact_types = trigger_data.get("fact_types")
|
||||
exclude_mental_models = trigger_data.get("exclude_mental_models", False)
|
||||
stored_exclude_ids: list[str] = trigger_data.get("exclude_mental_model_ids") or []
|
||||
recall_include_chunks_override = trigger_data.get("include_chunks")
|
||||
recall_max_tokens_override = trigger_data.get("recall_max_tokens")
|
||||
recall_chunks_max_tokens_override = trigger_data.get("recall_chunks_max_tokens")
|
||||
|
||||
tag_filtering = _resolve_refresh_tag_filtering(mental_model.get("tags"), trigger_data)
|
||||
|
||||
# Run reflect with the source query, excluding the mental model being refreshed
|
||||
# Skip creating a nested "hindsight.reflect" span since we already have "hindsight.mental_model_refresh"
|
||||
reflect_result = await self.reflect_async(
|
||||
reflect_kwargs: dict[str, Any] = dict(
|
||||
bank_id=bank_id,
|
||||
query=mental_model["source_query"],
|
||||
request_context=request_context,
|
||||
@@ -6785,8 +7052,17 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
fact_types=fact_types,
|
||||
exclude_mental_models=exclude_mental_models,
|
||||
exclude_mental_model_ids=list({*stored_exclude_ids, mental_model_id}),
|
||||
recall_include_chunks=recall_include_chunks_override,
|
||||
recall_max_tokens_override=recall_max_tokens_override,
|
||||
recall_chunks_max_tokens_override=recall_chunks_max_tokens_override,
|
||||
_skip_span=True,
|
||||
)
|
||||
# Forward the per-model max_tokens so the final synthesis is capped at the
|
||||
# user-configured limit rather than the reflect_async default.
|
||||
stored_max_tokens = mental_model.get("max_tokens")
|
||||
if stored_max_tokens is not None:
|
||||
reflect_kwargs["max_tokens"] = stored_max_tokens
|
||||
reflect_result = await self.reflect_async(**reflect_kwargs)
|
||||
|
||||
# Build reflect_response payload to store
|
||||
# based_on contains MemoryFact objects for most types, but plain dicts for directives
|
||||
@@ -7433,6 +7709,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
operation_id: str,
|
||||
*,
|
||||
request_context: "RequestContext",
|
||||
include_payload: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Get the status of a specific async operation.
|
||||
|
||||
@@ -7455,9 +7732,10 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
op_uuid = uuid.UUID(operation_id)
|
||||
|
||||
async with acquire_with_retry(pool) as conn:
|
||||
payload_column = ", task_payload" if include_payload else ""
|
||||
row = await conn.fetchrow(
|
||||
f"""
|
||||
SELECT operation_id, operation_type, created_at, updated_at, completed_at, status, error_message, result_metadata
|
||||
SELECT operation_id, operation_type, created_at, updated_at, completed_at, status, error_message, result_metadata{payload_column}
|
||||
FROM {fq_table("async_operations")}
|
||||
WHERE operation_id = $1 AND bank_id = $2
|
||||
""",
|
||||
@@ -7469,6 +7747,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
# Check if this is a parent operation
|
||||
result_metadata = json.loads(row["result_metadata"]) if row["result_metadata"] else {}
|
||||
is_parent = result_metadata.get("is_parent", False)
|
||||
task_payload = json.loads(row["task_payload"]) if include_payload and row["task_payload"] else None
|
||||
|
||||
# Use status from database (parent status is updated when all children complete/fail)
|
||||
db_status = row["status"]
|
||||
@@ -7545,6 +7824,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
"error_message": row["error_message"],
|
||||
"result_metadata": result_metadata,
|
||||
"child_operations": child_statuses,
|
||||
"task_payload": task_payload,
|
||||
}
|
||||
else:
|
||||
# Regular operation (not a parent)
|
||||
@@ -7557,6 +7837,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
"completed_at": row["completed_at"].isoformat() if row["completed_at"] else None,
|
||||
"error_message": row["error_message"],
|
||||
"result_metadata": result_metadata,
|
||||
"task_payload": task_payload,
|
||||
}
|
||||
else:
|
||||
# Operation not found
|
||||
|
||||
@@ -175,7 +175,7 @@ class GeminiLLM(LLMInterface):
|
||||
Args:
|
||||
messages: List of message dicts with 'role' and 'content'.
|
||||
response_format: Optional Pydantic model for structured output.
|
||||
max_completion_tokens: Maximum tokens in response (not supported by Gemini).
|
||||
max_completion_tokens: Maximum tokens in response (mapped to Gemini's max_output_tokens).
|
||||
temperature: Sampling temperature (0.0-2.0).
|
||||
scope: Scope identifier for tracking.
|
||||
max_retries: Maximum retry attempts.
|
||||
@@ -227,6 +227,11 @@ class GeminiLLM(LLMInterface):
|
||||
config_kwargs["response_schema"] = response_format
|
||||
if temperature is not None:
|
||||
config_kwargs["temperature"] = temperature
|
||||
# Gemini's equivalent of OpenAI-style max_completion_tokens is max_output_tokens.
|
||||
# Without it the model can produce arbitrarily long responses, ignoring the
|
||||
# caller's intended cap (e.g. mental_models max_tokens during refresh).
|
||||
if max_completion_tokens is not None:
|
||||
config_kwargs["max_output_tokens"] = max_completion_tokens
|
||||
|
||||
# Apply safety settings: context var (per-request bank override) takes precedence over instance default
|
||||
effective_safety_settings = _safety_settings_ctx.get()
|
||||
@@ -401,7 +406,7 @@ class GeminiLLM(LLMInterface):
|
||||
Args:
|
||||
messages: List of message dicts. Can include tool results with role='tool'.
|
||||
tools: List of tool definitions in OpenAI format.
|
||||
max_completion_tokens: Maximum tokens (not supported by Gemini).
|
||||
max_completion_tokens: Maximum tokens (mapped to Gemini's max_output_tokens).
|
||||
temperature: Sampling temperature.
|
||||
scope: Scope identifier for tracking.
|
||||
max_retries: Maximum retry attempts.
|
||||
@@ -493,6 +498,10 @@ class GeminiLLM(LLMInterface):
|
||||
config_kwargs["system_instruction"] = system_instruction
|
||||
if temperature is not None:
|
||||
config_kwargs["temperature"] = temperature
|
||||
# See note in `call`: Gemini's max_output_tokens is the equivalent of
|
||||
# OpenAI-style max_completion_tokens.
|
||||
if max_completion_tokens is not None:
|
||||
config_kwargs["max_output_tokens"] = max_completion_tokens
|
||||
|
||||
# Map OpenAI-style tool_choice to Gemini FunctionCallingConfig
|
||||
if tool_choice == "required":
|
||||
|
||||
@@ -652,6 +652,46 @@ async def run_reflect_agent(
|
||||
if result.content:
|
||||
answer = _clean_answer_text(result.content.strip())
|
||||
|
||||
# The call_with_tools call above is intentionally uncapped so the
|
||||
# LLM has headroom to emit tool-call JSON plus any intermediate
|
||||
# reasoning. But when the LLM short-circuits and returns text
|
||||
# directly, that text becomes the user-visible final answer and
|
||||
# must respect max_tokens like the forced-final paths do. If it
|
||||
# overshoots, run one extra capped call to rewrite it within
|
||||
# the cap.
|
||||
if max_tokens is not None and len(_TIKTOKEN_ENCODING.encode(answer)) > max_tokens:
|
||||
rewrite_start = time.time()
|
||||
rewritten, rewrite_usage = await llm_config.call(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Rewrite the user's text so it fits within the requested token "
|
||||
"budget. Preserve the key facts and structure; drop lower-priority "
|
||||
"detail. Respond with the rewritten text only, no preamble."
|
||||
),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Target budget: {max_tokens} tokens.\n\nText to rewrite:\n{answer}",
|
||||
},
|
||||
],
|
||||
scope="reflect",
|
||||
max_completion_tokens=max_tokens,
|
||||
return_usage=True,
|
||||
)
|
||||
total_input_tokens += rewrite_usage.input_tokens
|
||||
total_output_tokens += rewrite_usage.output_tokens
|
||||
llm_trace.append(
|
||||
{
|
||||
"scope": "final_rewrite",
|
||||
"duration_ms": int((time.time() - rewrite_start) * 1000),
|
||||
"input_tokens": rewrite_usage.input_tokens,
|
||||
"output_tokens": rewrite_usage.output_tokens,
|
||||
}
|
||||
)
|
||||
answer = _clean_answer_text(rewritten.strip())
|
||||
|
||||
# Generate structured output if schema provided
|
||||
structured_output = None
|
||||
if response_schema and answer:
|
||||
|
||||
@@ -214,6 +214,7 @@ async def tool_recall(
|
||||
connection_budget: int = 1,
|
||||
max_chunk_tokens: int = 1000,
|
||||
fact_types: list[str] | None = None,
|
||||
include_chunks: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Search memories using TEMPR retrieval.
|
||||
@@ -230,15 +231,15 @@ async def tool_recall(
|
||||
tags: Filter by tags (includes untagged memories)
|
||||
tags_match: How to match tags - "any" (OR), "all" (AND), or "exact"
|
||||
connection_budget: Max DB connections for this recall (default 1 for internal ops)
|
||||
max_chunk_tokens: Maximum tokens for raw source chunk text (default 1000, always included)
|
||||
max_chunk_tokens: Maximum tokens for raw source chunk text (default 1000)
|
||||
fact_types: Optional filter for fact types to retrieve. Defaults to ["experience", "world"].
|
||||
include_chunks: Whether to fetch raw chunk text alongside facts (default True).
|
||||
|
||||
Returns:
|
||||
Dict with list of matching memories including raw chunk text
|
||||
Dict with list of matching memories including raw chunk text (when include_chunks)
|
||||
"""
|
||||
# Only world/experience are valid for raw recall (observation is handled by search_observations)
|
||||
recall_fact_type = [ft for ft in (fact_types or ["experience", "world"]) if ft in ("world", "experience")]
|
||||
include_chunks = True
|
||||
internal_ctx = replace(request_context, internal=True)
|
||||
result = await memory_engine.recall_async(
|
||||
bank_id=bank_id,
|
||||
|
||||
@@ -47,6 +47,16 @@ async def generate_embeddings_batch(embeddings_backend, texts: list[str]) -> lis
|
||||
embeddings_backend.encode,
|
||||
texts,
|
||||
)
|
||||
return embeddings
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to generate batch embeddings: {str(e)}")
|
||||
|
||||
# Guarantee 1:1 alignment with input texts. A silent length mismatch here
|
||||
# propagates downstream as zip() drops items, eventually surfacing as an
|
||||
# IndexError in retain mapping (see issue #1037).
|
||||
if len(embeddings) != len(texts):
|
||||
raise RuntimeError(
|
||||
f"Embeddings backend returned {len(embeddings)} vectors for {len(texts)} input texts; "
|
||||
"expected exact 1:1 alignment"
|
||||
)
|
||||
|
||||
return embeddings
|
||||
|
||||
@@ -72,7 +72,6 @@ from . import (
|
||||
from .types import (
|
||||
ChunkMetadata,
|
||||
EntityResolutionResult,
|
||||
ExtractedFact,
|
||||
Phase1Result,
|
||||
Phase3Context,
|
||||
ProcessedFact,
|
||||
@@ -302,8 +301,11 @@ async def _insert_facts_and_links(
|
||||
causal_link_count = await link_creation.create_causal_links_batch(conn, bank_id, unit_ids, processed_facts)
|
||||
log_buffer.append(f" Causal links: {causal_link_count} links in {time.time() - step_start:.3f}s")
|
||||
|
||||
# Map results back to original content items
|
||||
result_unit_ids = _map_results_to_contents(contents, extracted_facts, unit_ids if unit_ids else [])
|
||||
# Map results back to original content items. Use processed_facts (not
|
||||
# extracted_facts) because unit_ids has 1:1 alignment with processed_facts —
|
||||
# any upstream drop between extraction and processing would otherwise cause
|
||||
# an IndexError (see issue #1037).
|
||||
result_unit_ids = _map_results_to_contents(contents, processed_facts, unit_ids if unit_ids else [])
|
||||
|
||||
if outbox_callback:
|
||||
await outbox_callback(conn)
|
||||
@@ -487,8 +489,8 @@ async def retain_batch(
|
||||
return result_unit_ids, total_usage
|
||||
|
||||
# Resolve effective document_id early so both delta and streaming paths
|
||||
# can find existing chunks from a prior attempt. On retry, the generated
|
||||
# document_id is recovered from operation result_metadata.
|
||||
# can find existing chunks from a prior attempt. On retry, a generated
|
||||
# document_id is recovered from operation result_metadata.document_ids[0].
|
||||
effective_doc_id = document_id
|
||||
if not effective_doc_id:
|
||||
doc_ids = {item.get("document_id") for item in contents_dicts if item.get("document_id")}
|
||||
@@ -507,26 +509,41 @@ async def retain_batch(
|
||||
if isinstance(row["result_metadata"], dict)
|
||||
else json.loads(row["result_metadata"])
|
||||
)
|
||||
effective_doc_id = meta.get("generated_document_id")
|
||||
recovered = meta.get("document_ids") or []
|
||||
if recovered:
|
||||
effective_doc_id = recovered[0]
|
||||
except Exception:
|
||||
pass
|
||||
if not effective_doc_id:
|
||||
effective_doc_id = str(uuid.uuid4())
|
||||
# Persist so retries reuse the same document_id
|
||||
if operation_id:
|
||||
try:
|
||||
async with acquire_with_retry(pool) as conn:
|
||||
await conn.execute(
|
||||
f"""
|
||||
UPDATE {fq_table("async_operations")}
|
||||
SET result_metadata = result_metadata || $1::jsonb, updated_at = now()
|
||||
WHERE operation_id = $2
|
||||
""",
|
||||
json.dumps({"generated_document_id": effective_doc_id}),
|
||||
uuid.UUID(operation_id),
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Failed to persist generated document_id", exc_info=True)
|
||||
|
||||
# Record effective_doc_id on the operation (idempotent set-append). Captures
|
||||
# both user-provided and generated ids so the operation shows every document
|
||||
# it touched, and lets retries reuse the same generated id.
|
||||
if operation_id:
|
||||
try:
|
||||
async with acquire_with_retry(pool) as conn:
|
||||
await conn.execute(
|
||||
f"""
|
||||
UPDATE {fq_table("async_operations")}
|
||||
SET result_metadata = jsonb_set(
|
||||
COALESCE(result_metadata, '{{}}'::jsonb),
|
||||
'{{document_ids}}',
|
||||
CASE
|
||||
WHEN COALESCE(result_metadata->'document_ids', '[]'::jsonb) @> $1::jsonb
|
||||
THEN result_metadata->'document_ids'
|
||||
ELSE COALESCE(result_metadata->'document_ids', '[]'::jsonb) || $1::jsonb
|
||||
END,
|
||||
true
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE operation_id = $2
|
||||
""",
|
||||
json.dumps([effective_doc_id]),
|
||||
uuid.UUID(operation_id),
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Failed to persist document_id", exc_info=True)
|
||||
|
||||
# --- Append mode: prepend existing document content to new content ---
|
||||
# When update_mode="append", fetch the existing document text and prepend it
|
||||
@@ -1550,12 +1567,19 @@ def _build_delta_contents(
|
||||
|
||||
def _map_results_to_contents(
|
||||
contents: list[RetainContent],
|
||||
extracted_facts: list[ExtractedFact],
|
||||
processed_facts: list[ProcessedFact],
|
||||
unit_ids: list[str],
|
||||
) -> list[list[str]]:
|
||||
"""Map created unit IDs back to original content items."""
|
||||
"""Map created unit IDs back to original content items.
|
||||
|
||||
`processed_facts` and `unit_ids` must have the same length: each unit_id
|
||||
corresponds to the processed_fact at the same index.
|
||||
"""
|
||||
if len(processed_facts) != len(unit_ids):
|
||||
raise ValueError(f"processed_facts ({len(processed_facts)}) and unit_ids ({len(unit_ids)}) length mismatch")
|
||||
|
||||
facts_by_content: dict[int, list[int]] = {i: [] for i in range(len(contents))}
|
||||
for i, fact in enumerate(extracted_facts):
|
||||
for i, fact in enumerate(processed_facts):
|
||||
# Normalize content_index: some LLM providers return 1-indexed values.
|
||||
# Clamp to valid range to prevent KeyError.
|
||||
idx = fact.content_index
|
||||
@@ -1564,12 +1588,8 @@ def _map_results_to_contents(
|
||||
facts_by_content[idx].append(i)
|
||||
|
||||
result_unit_ids = []
|
||||
unit_idx = 0
|
||||
for content_index in range(len(contents)):
|
||||
content_unit_ids = []
|
||||
for _ in facts_by_content[content_index]:
|
||||
content_unit_ids.append(unit_ids[unit_idx])
|
||||
unit_idx += 1
|
||||
content_unit_ids = [unit_ids[i] for i in facts_by_content[content_index]]
|
||||
result_unit_ids.append(content_unit_ids)
|
||||
|
||||
return result_unit_ids
|
||||
|
||||
@@ -812,13 +812,42 @@ class WorkerPoller:
|
||||
schemas = await self._get_schemas()
|
||||
global_pending = 0
|
||||
all_worker_counts: dict[str, int] = {}
|
||||
# operation_type -> aggregated bucket counts across schemas
|
||||
pending_breakdown: dict[str, dict[str, int]] = {}
|
||||
|
||||
async with self._pool.acquire() as conn:
|
||||
for schema in schemas:
|
||||
table = fq_table("async_operations", schema)
|
||||
|
||||
row = await conn.fetchrow(f"SELECT COUNT(*) as count FROM {table} WHERE status = 'pending'")
|
||||
global_pending += row["count"] if row else 0
|
||||
# Bucket pending rows by the same predicates the claim query
|
||||
# filters on, so an operator can see why pending > 0 but
|
||||
# nothing is being claimed (orphaned batch_retain parents,
|
||||
# retry backoff, etc.).
|
||||
breakdown_rows = await conn.fetch(
|
||||
f"""
|
||||
SELECT
|
||||
operation_type,
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE task_payload IS NULL) AS payload_null,
|
||||
COUNT(*) FILTER (
|
||||
WHERE next_retry_at IS NOT NULL AND next_retry_at > now()
|
||||
) AS retry_blocked,
|
||||
COUNT(*) FILTER (WHERE worker_id IS NOT NULL) AS assigned
|
||||
FROM {table}
|
||||
WHERE status = 'pending'
|
||||
GROUP BY operation_type
|
||||
"""
|
||||
)
|
||||
for br in breakdown_rows:
|
||||
op_type = br["operation_type"] or "unknown"
|
||||
bucket = pending_breakdown.setdefault(
|
||||
op_type, {"total": 0, "payload_null": 0, "retry_blocked": 0, "assigned": 0}
|
||||
)
|
||||
bucket["total"] += br["total"]
|
||||
bucket["payload_null"] += br["payload_null"]
|
||||
bucket["retry_blocked"] += br["retry_blocked"]
|
||||
bucket["assigned"] += br["assigned"]
|
||||
global_pending += br["total"]
|
||||
|
||||
worker_rows = await conn.fetch(
|
||||
f"""
|
||||
@@ -856,6 +885,13 @@ class WorkerPoller:
|
||||
f"my_active: {processing_str}"
|
||||
)
|
||||
|
||||
# Pending breakdown - explains why pending rows aren't being claimed
|
||||
# (orphaned batch_retain parents have payload_null > 0, retry storms
|
||||
# show up as retry_blocked, etc.). Skip when nothing is pending so
|
||||
# the line doesn't add noise on idle deployments.
|
||||
if global_pending > 0:
|
||||
self._log_pending_breakdown(pending_breakdown)
|
||||
|
||||
# Per-task lines, sorted oldest-first so stuck tasks bubble to the top.
|
||||
self._log_per_task_lines(active_tasks, now=time.monotonic())
|
||||
|
||||
@@ -910,6 +946,35 @@ class WorkerPoller:
|
||||
logger.debug(f"Pool stats unavailable: {e}")
|
||||
return "unavailable"
|
||||
|
||||
def _log_pending_breakdown(self, breakdown: dict[str, dict[str, int]]) -> None:
|
||||
"""Emit one [PENDING_BREAKDOWN] line bucketing pending rows by claimability.
|
||||
|
||||
Each bucket mirrors a predicate in the claim query:
|
||||
* payload_null - row has no task_payload (e.g. batch_retain parent
|
||||
whose reconciliation never fired); claim query
|
||||
skips it forever
|
||||
* retry_blocked - next_retry_at is still in the future
|
||||
* assigned - worker_id already set; another worker owns it
|
||||
|
||||
``claimable`` is the residual that *should* be picked up on the next
|
||||
poll. If ``claimable > 0`` while workers report free slots, the bug is
|
||||
somewhere else (lock contention, tenant discovery, etc.) - this line
|
||||
narrows the search.
|
||||
"""
|
||||
if not breakdown:
|
||||
return
|
||||
|
||||
parts = []
|
||||
for op_type in sorted(breakdown):
|
||||
b = breakdown[op_type]
|
||||
claimable = b["total"] - b["payload_null"] - b["retry_blocked"] - b["assigned"]
|
||||
parts.append(
|
||||
f"{op_type}: total={b['total']} claimable={claimable} "
|
||||
f"payload_null={b['payload_null']} retry_blocked={b['retry_blocked']} "
|
||||
f"assigned={b['assigned']}"
|
||||
)
|
||||
logger.info(f"[PENDING_BREAKDOWN] {' | '.join(parts)}")
|
||||
|
||||
def _log_per_task_lines(self, active_tasks: dict[str, ActiveTaskInfo], now: float) -> None:
|
||||
"""Emit one [WORKER_TASK] line per in-flight task and dump stuck stacks.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-api-slim"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
description = "Hindsight: Agent Memory That Works Like Human Memory"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -433,3 +433,130 @@ async def test_config_retain_batch_tokens_respected(memory, request_context):
|
||||
# Even small batches use parent-child pattern now (simpler code path)
|
||||
assert "child_operations" in status
|
||||
assert status["result_metadata"]["num_sub_batches"] == 1
|
||||
|
||||
|
||||
async def _child_metadata(memory, bank_id: str, parent_operation_id: str, request_context):
|
||||
"""Fetch the first child operation's result_metadata for a parent batch_retain."""
|
||||
parent = await memory.get_operation_status(
|
||||
bank_id=bank_id,
|
||||
operation_id=parent_operation_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
assert parent["status"] == "completed", parent
|
||||
assert parent["child_operations"], "expected at least one child operation"
|
||||
child_id = parent["child_operations"][0]["operation_id"]
|
||||
child = await memory.get_operation_status(
|
||||
bank_id=bank_id,
|
||||
operation_id=child_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
return child["result_metadata"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retain_records_user_provided_document_ids(memory, request_context):
|
||||
"""User-supplied document_ids land in child op result_metadata.document_ids."""
|
||||
bank_id = "test_doc_ids_user_supplied"
|
||||
d1 = str(uuid.uuid4())
|
||||
d2 = str(uuid.uuid4())
|
||||
contents = [
|
||||
{"content": "User-supplied doc one content.", "document_id": d1},
|
||||
{"content": "User-supplied doc two content.", "document_id": d2},
|
||||
]
|
||||
|
||||
result = await memory.submit_async_retain(
|
||||
bank_id=bank_id,
|
||||
contents=contents,
|
||||
request_context=request_context,
|
||||
)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
meta = await _child_metadata(memory, bank_id, result["operation_id"], request_context)
|
||||
assert "document_ids" in meta, meta
|
||||
assert set(meta["document_ids"]) == {d1, d2}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retain_records_generated_document_id(memory, request_context):
|
||||
"""With no document_ids supplied, retain records the single generated id."""
|
||||
bank_id = "test_doc_ids_generated"
|
||||
contents = [
|
||||
{"content": "Generated doc item one."},
|
||||
{"content": "Generated doc item two."},
|
||||
]
|
||||
|
||||
result = await memory.submit_async_retain(
|
||||
bank_id=bank_id,
|
||||
contents=contents,
|
||||
request_context=request_context,
|
||||
)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
meta = await _child_metadata(memory, bank_id, result["operation_id"], request_context)
|
||||
assert "document_ids" in meta, meta
|
||||
assert isinstance(meta["document_ids"], list)
|
||||
assert len(meta["document_ids"]) == 1
|
||||
# Must be a valid UUID string (generated by the orchestrator)
|
||||
uuid.UUID(meta["document_ids"][0])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retain_records_shared_document_id_once(memory, request_context):
|
||||
"""Items sharing one document_id record it exactly once (idempotent set-append)."""
|
||||
bank_id = "test_doc_ids_shared"
|
||||
shared = str(uuid.uuid4())
|
||||
# Duplicate per-item doc_ids are rejected up front, so shared-doc mode
|
||||
# is exercised by a single item carrying the id.
|
||||
contents = [{"content": "Shared doc, chunk A.", "document_id": shared}]
|
||||
|
||||
result = await memory.submit_async_retain(
|
||||
bank_id=bank_id,
|
||||
contents=contents,
|
||||
request_context=request_context,
|
||||
)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
meta = await _child_metadata(memory, bank_id, result["operation_id"], request_context)
|
||||
assert meta.get("document_ids") == [shared]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_operation_status_include_payload(memory, request_context):
|
||||
"""include_payload=True returns the original submission payload; default omits it."""
|
||||
bank_id = "test_include_payload"
|
||||
contents = [{"content": "Payload roundtrip test item."}]
|
||||
|
||||
result = await memory.submit_async_retain(
|
||||
bank_id=bank_id,
|
||||
contents=contents,
|
||||
request_context=request_context,
|
||||
)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
parent = await memory.get_operation_status(
|
||||
bank_id=bank_id,
|
||||
operation_id=result["operation_id"],
|
||||
request_context=request_context,
|
||||
)
|
||||
child_id = parent["child_operations"][0]["operation_id"]
|
||||
|
||||
# Default: no payload
|
||||
without = await memory.get_operation_status(
|
||||
bank_id=bank_id,
|
||||
operation_id=child_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
assert without.get("task_payload") is None
|
||||
|
||||
# With flag: payload populated
|
||||
with_payload = await memory.get_operation_status(
|
||||
bank_id=bank_id,
|
||||
operation_id=child_id,
|
||||
request_context=request_context,
|
||||
include_payload=True,
|
||||
)
|
||||
payload = with_payload.get("task_payload")
|
||||
assert payload is not None, with_payload
|
||||
assert payload.get("bank_id") == bank_id
|
||||
assert payload.get("contents")
|
||||
assert payload["contents"][0]["content"] == "Payload roundtrip test item."
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Tests for the bank stats endpoint and the memories-timeseries endpoint.
|
||||
|
||||
Covers the new fields exposed by GET /v1/default/banks/{bank_id}/stats
|
||||
(operations_by_status) and the new endpoint
|
||||
GET /v1/default/banks/{bank_id}/stats/memories-timeseries.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from hindsight_api.api import create_app
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def api_client(memory):
|
||||
app = create_app(memory, initialize_memory=False)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_bank_id():
|
||||
return f"stats_test_{datetime.now().timestamp()}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bank_stats_exposes_operations_by_status(api_client, test_bank_id):
|
||||
"""/stats should return operations_by_status with all finished operations."""
|
||||
try:
|
||||
# Kick off a retain so at least one completed operation exists.
|
||||
response = await api_client.post(
|
||||
f"/v1/default/banks/{test_bank_id}/memories",
|
||||
json={"items": [{"content": "Alice is a software engineer.", "context": "team"}]},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/stats")
|
||||
assert response.status_code == 200
|
||||
stats = response.json()
|
||||
|
||||
assert "operations_by_status" in stats
|
||||
assert isinstance(stats["operations_by_status"], dict)
|
||||
# A synchronous retain finishes as "completed".
|
||||
assert stats["operations_by_status"].get("completed", 0) >= 1
|
||||
# pending/failed counters should still be present as scalar mirrors.
|
||||
assert stats["pending_operations"] == stats["operations_by_status"].get("pending", 0)
|
||||
assert stats["failed_operations"] == stats["operations_by_status"].get("failed", 0)
|
||||
finally:
|
||||
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"period,expected_count,expected_trunc",
|
||||
[
|
||||
("1h", 60, "minute"),
|
||||
("12h", 12, "hour"),
|
||||
("1d", 24, "hour"),
|
||||
("7d", 7, "day"),
|
||||
("30d", 30, "day"),
|
||||
("90d", 90, "day"),
|
||||
],
|
||||
)
|
||||
async def test_memories_timeseries_periods(
|
||||
api_client, test_bank_id, period, expected_count, expected_trunc
|
||||
):
|
||||
"""Every period must return the full expected bucket count and trunc."""
|
||||
try:
|
||||
response = await api_client.post(
|
||||
f"/v1/default/banks/{test_bank_id}/memories",
|
||||
json={"items": [{"content": "Bob works on infrastructure.", "context": "team"}]},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = await api_client.get(
|
||||
f"/v1/default/banks/{test_bank_id}/stats/memories-timeseries",
|
||||
params={"period": period},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
|
||||
assert body["bank_id"] == test_bank_id
|
||||
assert body["period"] == period
|
||||
assert body["trunc"] == expected_trunc
|
||||
assert len(body["buckets"]) == expected_count
|
||||
|
||||
for bucket in body["buckets"]:
|
||||
assert "time" in bucket
|
||||
assert bucket["world"] >= 0
|
||||
assert bucket["experience"] >= 0
|
||||
assert bucket["observation"] >= 0
|
||||
finally:
|
||||
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_memories_timeseries_invalid_period_falls_back(api_client, test_bank_id):
|
||||
"""An unknown period must fall back to the 7d default."""
|
||||
try:
|
||||
response = await api_client.get(
|
||||
f"/v1/default/banks/{test_bank_id}/stats/memories-timeseries",
|
||||
params={"period": "nonsense"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["period"] == "7d"
|
||||
assert body["trunc"] == "day"
|
||||
assert len(body["buckets"]) == 7
|
||||
finally:
|
||||
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_memories_timeseries_empty_bank_returns_zero_filled_buckets(
|
||||
api_client, test_bank_id
|
||||
):
|
||||
"""A bank with no memories must still return the full zero-filled bucket set."""
|
||||
try:
|
||||
# Ensure the bank exists.
|
||||
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
|
||||
assert response.status_code == 200
|
||||
|
||||
response = await api_client.get(
|
||||
f"/v1/default/banks/{test_bank_id}/stats/memories-timeseries",
|
||||
params={"period": "7d"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert len(body["buckets"]) == 7
|
||||
for bucket in body["buckets"]:
|
||||
assert bucket["world"] == 0
|
||||
assert bucket["experience"] == 0
|
||||
assert bucket["observation"] == 0
|
||||
finally:
|
||||
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_memories_timeseries_reflects_retained_memories(api_client, test_bank_id):
|
||||
"""Freshly-retained memories must show up in today's bucket counts."""
|
||||
try:
|
||||
response = await api_client.post(
|
||||
f"/v1/default/banks/{test_bank_id}/memories",
|
||||
json={
|
||||
"items": [
|
||||
{"content": "Alice is a software engineer.", "context": "team"},
|
||||
{"content": "Bob works on infrastructure.", "context": "team"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = await api_client.get(
|
||||
f"/v1/default/banks/{test_bank_id}/stats/memories-timeseries",
|
||||
params={"period": "7d"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
totals = sum(b["world"] + b["experience"] + b["observation"] for b in body["buckets"])
|
||||
assert totals >= 2, "expected at least two memories across all buckets"
|
||||
|
||||
# Those memories should land in the most-recent bucket.
|
||||
latest = body["buckets"][-1]
|
||||
assert latest["world"] + latest["experience"] + latest["observation"] >= 2
|
||||
finally:
|
||||
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Verify that BankTemplateConfig exposes every hierarchical field that
|
||||
_CONFIGURABLE_FIELDS already accepts at the engine layer.
|
||||
|
||||
This test guards the fix for the gap described in the upstream PR title
|
||||
"fix(bank-template): align BankTemplateConfig with _CONFIGURABLE_FIELDS".
|
||||
Each new field is POSTed through /v1/default/banks/{id}/import and then
|
||||
read back via the bank-config endpoint; assertion is that the applied
|
||||
value round-trips through the engine.
|
||||
|
||||
Runs via: uv run pytest tests/test_bank_template_configurable_fields.py -v
|
||||
|
||||
The api_client fixture (shared with tests/test_bank_templates.py) wraps
|
||||
create_app(memory, initialize_memory=False) in an httpx.ASGITransport
|
||||
with base_url http://test — in-process, no network, no tenant extension.
|
||||
Copy the fixture inline here so the test file does not depend on a
|
||||
conftest we do not ship in the patch.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from hindsight_api.api import create_app
|
||||
from hindsight_api.api.http import BankTemplateConfig
|
||||
|
||||
# Each tuple is (field_name, applied_value). Values chosen to differ
|
||||
# visibly from defaults so round-trip bugs surface.
|
||||
NEW_FIELDS: list[tuple[str, object]] = [
|
||||
("retain_default_strategy", "strategy-a"),
|
||||
("retain_strategies", {"strategy-a": {"mode": "concise", "max_tokens": 512}}),
|
||||
("retain_chunk_batch_size", 7),
|
||||
("mcp_enabled_tools", ["list_banks", "get_bank_profile"]),
|
||||
("consolidation_llm_batch_size", 11),
|
||||
("consolidation_source_facts_max_tokens", 2048),
|
||||
("consolidation_source_facts_max_tokens_per_observation", 256),
|
||||
("max_observations_per_scope", 13),
|
||||
("reflect_source_facts_max_tokens", 4096),
|
||||
("llm_gemini_safety_settings", [{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}]),
|
||||
]
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def api_client(memory):
|
||||
"""Matches the fixture in tests/test_bank_templates.py — in-process
|
||||
ASGI test client, no tenant extension, no auth."""
|
||||
app = create_app(memory, initialize_memory=False)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bank_id():
|
||||
return f"tmpl_config_{datetime.now().timestamp()}"
|
||||
|
||||
|
||||
def test_bank_template_config_declares_every_configurable_field():
|
||||
"""Pydantic-level guard: every field in NEW_FIELDS must be a declared
|
||||
attribute of BankTemplateConfig so get_config_updates() picks it up."""
|
||||
declared = set(BankTemplateConfig.model_fields.keys())
|
||||
missing = [name for name, _ in NEW_FIELDS if name not in declared]
|
||||
assert not missing, f"BankTemplateConfig missing fields: {missing}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("field_name,applied_value", NEW_FIELDS, ids=[n for n, _ in NEW_FIELDS])
|
||||
async def test_new_field_round_trips_through_import(
|
||||
api_client: httpx.AsyncClient,
|
||||
bank_id: str,
|
||||
field_name: str,
|
||||
applied_value: object,
|
||||
):
|
||||
"""POST a minimal manifest with one new field set, then read bank
|
||||
config back and assert the value made it through.
|
||||
|
||||
Bank config response shape per upstream's test_import_applies_config:
|
||||
top-level keys are resolved hierarchical config; per-bank overrides
|
||||
live under config["overrides"][<field>]. Assert on the override slot.
|
||||
"""
|
||||
unique_bank_id = f"{bank_id}_{field_name}"
|
||||
manifest = {
|
||||
"version": "1",
|
||||
"bank": {field_name: applied_value},
|
||||
}
|
||||
|
||||
resp = await api_client.post(
|
||||
f"/v1/default/banks/{unique_bank_id}/import",
|
||||
json=manifest,
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
# Read bank config back — field must reflect the applied value
|
||||
# under the "overrides" slot, matching upstream's own test shape.
|
||||
read = await api_client.get(f"/v1/default/banks/{unique_bank_id}/config")
|
||||
assert read.status_code == 200, read.text
|
||||
config = read.json()
|
||||
overrides = config.get("overrides", {})
|
||||
assert overrides.get(field_name) == applied_value, (
|
||||
f"round-trip mismatch for {field_name}: "
|
||||
f"sent {applied_value!r}, got {overrides.get(field_name)!r} "
|
||||
f"(full overrides: {overrides!r})"
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Tests for consolidation retry budget configurability (issue #1042)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from hindsight_api.engine.consolidation.consolidator import _consolidate_batch_with_llm
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_llm_config():
|
||||
llm = AsyncMock()
|
||||
response = MagicMock()
|
||||
response.creates = []
|
||||
response.updates = []
|
||||
response.deletes = []
|
||||
llm.call.return_value = response
|
||||
return llm
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
config = MagicMock()
|
||||
config.observations_mission = None
|
||||
config.consolidation_max_attempts = 3
|
||||
config.consolidation_llm_max_retries = None
|
||||
return config
|
||||
|
||||
|
||||
class TestConsolidationRetryBudget:
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_is_required(self, mock_llm_config):
|
||||
"""Passing config=None raises — it's a programmer error, not a runtime fallback."""
|
||||
with pytest.raises(ValueError, match="config is required"):
|
||||
await _consolidate_batch_with_llm(
|
||||
llm_config=mock_llm_config,
|
||||
memories=[{"id": "m1", "text": "test"}],
|
||||
union_observations=[],
|
||||
union_source_facts={},
|
||||
config=None,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_configurable_max_attempts(self, mock_llm_config, mock_config):
|
||||
"""consolidation_max_attempts controls the outer retry loop."""
|
||||
mock_config.consolidation_max_attempts = 5
|
||||
mock_llm_config.call.side_effect = RuntimeError("fail")
|
||||
result = await _consolidate_batch_with_llm(
|
||||
llm_config=mock_llm_config,
|
||||
memories=[{"id": "m1", "text": "test"}],
|
||||
union_observations=[],
|
||||
union_source_facts={},
|
||||
config=mock_config,
|
||||
)
|
||||
assert result.failed
|
||||
assert mock_llm_config.call.call_count == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_retries_threaded_to_call(self, mock_llm_config, mock_config):
|
||||
"""consolidation_llm_max_retries is passed to llm_config.call()."""
|
||||
mock_config.consolidation_llm_max_retries = 3
|
||||
await _consolidate_batch_with_llm(
|
||||
llm_config=mock_llm_config,
|
||||
memories=[{"id": "m1", "text": "test"}],
|
||||
union_observations=[],
|
||||
union_source_facts={},
|
||||
config=mock_config,
|
||||
)
|
||||
assert mock_llm_config.call.call_args.kwargs.get("max_retries") == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_retries_not_passed_when_none(self, mock_llm_config, mock_config):
|
||||
"""When consolidation_llm_max_retries is None, max_retries is not passed."""
|
||||
mock_config.consolidation_llm_max_retries = None
|
||||
await _consolidate_batch_with_llm(
|
||||
llm_config=mock_llm_config,
|
||||
memories=[{"id": "m1", "text": "test"}],
|
||||
union_observations=[],
|
||||
union_source_facts={},
|
||||
config=mock_config,
|
||||
)
|
||||
assert "max_retries" not in mock_llm_config.call.call_args.kwargs
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reduced_budget_limits_total_calls(self, mock_llm_config, mock_config):
|
||||
"""Setting both to low values caps total failure attempts."""
|
||||
mock_config.consolidation_max_attempts = 2
|
||||
mock_config.consolidation_llm_max_retries = 2
|
||||
mock_llm_config.call.side_effect = RuntimeError("upstream 503")
|
||||
result = await _consolidate_batch_with_llm(
|
||||
llm_config=mock_llm_config,
|
||||
memories=[{"id": "m1", "text": "test"}],
|
||||
union_observations=[],
|
||||
union_source_facts={},
|
||||
config=mock_config,
|
||||
)
|
||||
assert result.failed
|
||||
assert mock_llm_config.call.call_count == 2
|
||||
for call_args in mock_llm_config.call.call_args_list:
|
||||
assert call_args.kwargs.get("max_retries") == 2
|
||||
@@ -98,7 +98,7 @@ async def test_hierarchical_fields_categorization():
|
||||
assert "retain_chunk_batch_size" in configurable
|
||||
|
||||
# Verify count is correct
|
||||
assert len(configurable) == 22
|
||||
assert len(configurable) == 25
|
||||
|
||||
# Verify credential fields (NEVER exposed)
|
||||
assert "llm_api_key" in credentials
|
||||
@@ -458,7 +458,7 @@ async def test_config_get_bank_config_no_static_or_credential_fields_leak(memory
|
||||
assert field in config, f"Expected configurable field '{field}' missing from config"
|
||||
|
||||
# Should have a small number of configurable fields (not hundreds)
|
||||
assert len(config) < 25, f"Too many fields returned: {len(config)}"
|
||||
assert len(config) < 30, f"Too many fields returned: {len(config)}"
|
||||
|
||||
finally:
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
@@ -286,6 +286,34 @@ async def test_full_api_workflow(api_client, test_bank_id):
|
||||
)
|
||||
assert response.status_code == 410 # Deprecated endpoint
|
||||
|
||||
# Entity co-occurrence graph — shape is stable even when there are no
|
||||
# co-occurrences; every edge must reference two nodes that are also present.
|
||||
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/entities/graph")
|
||||
assert response.status_code == 200
|
||||
entity_graph = response.json()
|
||||
assert set(entity_graph.keys()) >= {"nodes", "edges", "total_entities", "total_edges", "limit"}
|
||||
assert entity_graph["limit"] == 1000
|
||||
assert len(entity_graph["nodes"]) == entity_graph["total_entities"]
|
||||
assert len(entity_graph["edges"]) == entity_graph["total_edges"]
|
||||
node_ids = {n["data"]["id"] for n in entity_graph["nodes"]}
|
||||
for edge in entity_graph["edges"]:
|
||||
assert edge["data"]["source"] in node_ids
|
||||
assert edge["data"]["target"] in node_ids
|
||||
assert edge["data"]["linkType"] == "cooccurrence"
|
||||
assert edge["data"]["weight"] >= 1
|
||||
|
||||
# min_count filter — raising the threshold can only shrink the edge set.
|
||||
response = await api_client.get(
|
||||
f"/v1/default/banks/{test_bank_id}/entities/graph?min_count=1000000"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
filtered_graph = response.json()
|
||||
assert filtered_graph["total_edges"] == 0
|
||||
|
||||
# "graph" must route to the graph endpoint, not be parsed as an entity_id.
|
||||
# Regression guard in case someone reorders the FastAPI route registration.
|
||||
assert entity_graph["total_entities"] >= 0
|
||||
|
||||
# ================================================================
|
||||
# 9. List All Banks (should include our test bank)
|
||||
# ================================================================
|
||||
|
||||
@@ -25,6 +25,8 @@ from hindsight_api.engine.llm_wrapper import TokenUsage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
pytestmark = pytest.mark.xdist_group("load_batch_tests")
|
||||
|
||||
|
||||
def generate_content(char_count: int) -> str:
|
||||
"""Generate realistic content of approximately char_count characters."""
|
||||
@@ -117,9 +119,18 @@ class TestLargeBatchRetain:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@pytest.fixture
|
||||
def disable_observations(self):
|
||||
from hindsight_api.config import _get_raw_config
|
||||
config = _get_raw_config()
|
||||
original = config.enable_observations
|
||||
config.enable_observations = False
|
||||
yield
|
||||
config.enable_observations = original
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(300) # 5 minute timeout
|
||||
async def test_large_batch_500k_chars_20_items(self, memory_with_mock_llm, request_context):
|
||||
async def test_large_batch_500k_chars_20_items(self, memory_with_mock_llm, request_context, disable_observations):
|
||||
"""
|
||||
Test retaining a batch of 20 content items totaling ~500k chars.
|
||||
|
||||
@@ -283,7 +294,7 @@ class TestLargeBatchRetain:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(60)
|
||||
async def test_db_connection_pool_under_load(self, memory_with_mock_llm, request_context):
|
||||
async def test_db_connection_pool_under_load(self, memory_with_mock_llm, request_context, disable_observations):
|
||||
"""
|
||||
Test that DB connection pool handles concurrent operations.
|
||||
|
||||
|
||||
@@ -1253,6 +1253,145 @@ class TestMentalModelTriggerTagsConfig:
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
class TestMentalModelRefreshMaxTokens:
|
||||
"""Verify that refresh_mental_model honors the per-model max_tokens column.
|
||||
|
||||
These tests mock the engine's collaborators so we can assert the exact kwargs
|
||||
passed to reflect_async without spinning up a DB or LLM. The bug being guarded
|
||||
against: the per-model ``max_tokens`` column was ignored during refresh, so
|
||||
reflect_async fell back to its default (4096) and the generated content could
|
||||
exceed the user-configured limit when there were many facts to synthesize.
|
||||
"""
|
||||
|
||||
async def test_refresh_passes_stored_max_tokens_to_reflect(self, request_context):
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from hindsight_api.engine.memory_engine import MemoryEngine
|
||||
from hindsight_api.engine.response_models import ReflectResult
|
||||
|
||||
custom_max_tokens = 777
|
||||
mental_model = {
|
||||
"id": "mm-1",
|
||||
"bank_id": "bank-1",
|
||||
"name": "Capped Model",
|
||||
"source_query": "Summarize the facts",
|
||||
"content": "initial",
|
||||
"tags": None,
|
||||
"max_tokens": custom_max_tokens,
|
||||
"trigger": {"refresh_after_consolidation": False},
|
||||
}
|
||||
|
||||
engine = MemoryEngine.__new__(MemoryEngine)
|
||||
engine._authenticate_tenant = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
engine.get_mental_model = AsyncMock(return_value=mental_model) # type: ignore[method-assign]
|
||||
engine.reflect_async = AsyncMock( # type: ignore[method-assign]
|
||||
return_value=ReflectResult(text="stub synthesis", based_on={})
|
||||
)
|
||||
engine.update_mental_model = AsyncMock(return_value=mental_model) # type: ignore[method-assign]
|
||||
|
||||
await engine.refresh_mental_model(
|
||||
bank_id="bank-1",
|
||||
mental_model_id="mm-1",
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert engine.reflect_async.await_count == 1
|
||||
kwargs = engine.reflect_async.await_args.kwargs
|
||||
assert kwargs.get("max_tokens") == custom_max_tokens, (
|
||||
f"refresh_mental_model should forward the stored max_tokens ({custom_max_tokens}) "
|
||||
f"to reflect_async, but got max_tokens={kwargs.get('max_tokens')!r}"
|
||||
)
|
||||
|
||||
async def test_refresh_content_respects_max_tokens(self, memory: MemoryEngine, request_context):
|
||||
"""End-to-end: refreshed content must stay within the model's max_tokens cap.
|
||||
|
||||
We seed the bank with enough varied facts that an unconstrained synthesis
|
||||
would happily produce a long answer, then refresh a mental model with a
|
||||
small max_tokens and assert the resulting content is actually within the
|
||||
cap (with a small tolerance for cross-tokenizer drift, since the LLM may
|
||||
not use cl100k_base).
|
||||
"""
|
||||
from hindsight_api.engine.memory_engine import count_tokens
|
||||
|
||||
bank_id = f"test-refresh-cap-{uuid.uuid4().hex[:8]}"
|
||||
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
|
||||
# Seed enough content that an uncapped reflect would produce a long answer.
|
||||
await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=[
|
||||
{"content": (
|
||||
"Alice is the staff frontend engineer. She owns the design system, "
|
||||
"leads accessibility reviews, mentors three junior engineers, and runs "
|
||||
"the weekly UI guild meeting every Thursday at 2pm Pacific."
|
||||
)},
|
||||
{"content": (
|
||||
"Bob is the backend tech lead. He owns the payments service, the "
|
||||
"billing reconciliation pipeline, and the on-call rotation for the "
|
||||
"platform team. He is the primary reviewer for any database migration."
|
||||
)},
|
||||
{"content": (
|
||||
"Carol manages the data platform. Her team operates the warehouse, "
|
||||
"the streaming ingestion layer, and the metrics pipeline that feeds "
|
||||
"the executive dashboards refreshed every fifteen minutes."
|
||||
)},
|
||||
{"content": (
|
||||
"The team holds a company-wide demo every other Friday. Engineering "
|
||||
"presents shipped work, design walks through prototypes, and product "
|
||||
"shares roadmap updates for the upcoming quarter."
|
||||
)},
|
||||
{"content": (
|
||||
"Dan is the security lead. He runs the quarterly threat-modeling "
|
||||
"exercises, owns the incident response runbook, and coordinates the "
|
||||
"annual external penetration test with the vendor."
|
||||
)},
|
||||
{"content": (
|
||||
"Erin runs developer experience. She maintains the local-dev tooling, "
|
||||
"the CI pipelines, the release automation, and the internal "
|
||||
"documentation portal that everyone uses to onboard new hires."
|
||||
)},
|
||||
],
|
||||
request_context=request_context,
|
||||
)
|
||||
await memory.wait_for_background_tasks()
|
||||
|
||||
cap = 200
|
||||
mm = await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name="Team Summary (capped)",
|
||||
source_query="Give me a complete overview of every team member, what they own, and the recurring meetings.",
|
||||
content="initial",
|
||||
max_tokens=cap,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
refreshed = await memory.refresh_mental_model(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=mm["id"],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert refreshed is not None
|
||||
content = refreshed["content"]
|
||||
assert content, "refresh produced empty content"
|
||||
|
||||
# The provider enforces the cap exactly in its own tokenizer, but our
|
||||
# local count uses tiktoken (cl100k_base) which can disagree with
|
||||
# provider tokenizers (Gemini's SentencePiece in particular tends to run
|
||||
# ~30% higher for English prose). We use a generous tolerance — the test
|
||||
# is guarding against the regression where the cap was ignored entirely
|
||||
# and content grew toward reflect_async's default of 4096 tokens.
|
||||
observed_tokens = count_tokens(content)
|
||||
tolerance = 1.5
|
||||
assert observed_tokens <= cap * tolerance, (
|
||||
f"refreshed content exceeds max_tokens cap: "
|
||||
f"observed≈{observed_tokens} tokens, cap={cap} (tolerance x{tolerance}). "
|
||||
f"content={content!r}"
|
||||
)
|
||||
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
class TestMentalModelTriggerSchema:
|
||||
"""Unit tests for MentalModelTrigger schema validation (no DB needed)."""
|
||||
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Tests for the internal recall configuration knobs used during mental model
|
||||
refresh: recall_include_chunks, recall_max_tokens, recall_chunks_max_tokens.
|
||||
|
||||
These are exposed both as hierarchical config fields (env → tenant → bank)
|
||||
and as overrides on a mental model's `trigger` JSONB field.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api.engine.reflect.tools import tool_recall
|
||||
from hindsight_api.engine.response_models import RecallResult as RecallResultModel
|
||||
from hindsight_api.models import RequestContext
|
||||
|
||||
|
||||
def _make_mock_engine():
|
||||
engine = MagicMock()
|
||||
engine.recall_async = AsyncMock(return_value=RecallResultModel(results=[], entities={}, chunks={}))
|
||||
return engine
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_request_context():
|
||||
# internal=True bypasses the tenant extension, letting these unit tests
|
||||
# exercise engine methods without standing up auth.
|
||||
return RequestContext(internal=True)
|
||||
|
||||
|
||||
class TestToolRecallIncludeChunks:
|
||||
"""tool_recall must honor the include_chunks parameter (was hardcoded True)."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_includes_chunks(self, mock_request_context):
|
||||
engine = _make_mock_engine()
|
||||
|
||||
await tool_recall(engine, "bank-1", "q", mock_request_context)
|
||||
|
||||
kwargs = engine.recall_async.call_args.kwargs
|
||||
assert kwargs["include_chunks"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_include_chunks_false_propagates(self, mock_request_context):
|
||||
engine = _make_mock_engine()
|
||||
|
||||
await tool_recall(engine, "bank-1", "q", mock_request_context, include_chunks=False)
|
||||
|
||||
kwargs = engine.recall_async.call_args.kwargs
|
||||
assert kwargs["include_chunks"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_chunk_tokens_propagates(self, mock_request_context):
|
||||
engine = _make_mock_engine()
|
||||
|
||||
await tool_recall(
|
||||
engine, "bank-1", "q", mock_request_context, max_chunk_tokens=2500, max_tokens=512
|
||||
)
|
||||
|
||||
kwargs = engine.recall_async.call_args.kwargs
|
||||
assert kwargs["max_chunk_tokens"] == 2500
|
||||
assert kwargs["max_tokens"] == 512
|
||||
|
||||
|
||||
class TestRecallConfigFields:
|
||||
"""Hierarchical config fields for internal recall."""
|
||||
|
||||
def test_fields_exist_on_dataclass(self):
|
||||
from hindsight_api.config import HindsightConfig
|
||||
|
||||
names = {f.name for f in dataclasses.fields(HindsightConfig)}
|
||||
assert "recall_include_chunks" in names
|
||||
assert "recall_max_tokens" in names
|
||||
assert "recall_chunks_max_tokens" in names
|
||||
|
||||
def test_fields_are_configurable(self):
|
||||
from hindsight_api.config import HindsightConfig
|
||||
|
||||
configurable = HindsightConfig.get_configurable_fields()
|
||||
assert "recall_include_chunks" in configurable
|
||||
assert "recall_max_tokens" in configurable
|
||||
assert "recall_chunks_max_tokens" in configurable
|
||||
|
||||
def test_default_values(self):
|
||||
from hindsight_api.config import (
|
||||
DEFAULT_RECALL_CHUNKS_MAX_TOKENS,
|
||||
DEFAULT_RECALL_INCLUDE_CHUNKS,
|
||||
DEFAULT_RECALL_MAX_TOKENS,
|
||||
)
|
||||
|
||||
assert DEFAULT_RECALL_INCLUDE_CHUNKS is True
|
||||
assert DEFAULT_RECALL_MAX_TOKENS == 2048
|
||||
assert DEFAULT_RECALL_CHUNKS_MAX_TOKENS == 1000
|
||||
|
||||
def test_env_var_constants(self):
|
||||
from hindsight_api.config import (
|
||||
ENV_RECALL_CHUNKS_MAX_TOKENS,
|
||||
ENV_RECALL_INCLUDE_CHUNKS,
|
||||
ENV_RECALL_MAX_TOKENS,
|
||||
)
|
||||
|
||||
assert ENV_RECALL_INCLUDE_CHUNKS == "HINDSIGHT_API_RECALL_INCLUDE_CHUNKS"
|
||||
assert ENV_RECALL_MAX_TOKENS == "HINDSIGHT_API_RECALL_MAX_TOKENS"
|
||||
assert ENV_RECALL_CHUNKS_MAX_TOKENS == "HINDSIGHT_API_RECALL_CHUNKS_MAX_TOKENS"
|
||||
|
||||
@patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"HINDSIGHT_API_RECALL_INCLUDE_CHUNKS": "false",
|
||||
"HINDSIGHT_API_RECALL_MAX_TOKENS": "777",
|
||||
"HINDSIGHT_API_RECALL_CHUNKS_MAX_TOKENS": "333",
|
||||
},
|
||||
)
|
||||
def test_from_env_reads_overrides(self):
|
||||
from hindsight_api.config import HindsightConfig
|
||||
|
||||
config = HindsightConfig.from_env()
|
||||
assert config.recall_include_chunks is False
|
||||
assert config.recall_max_tokens == 777
|
||||
assert config.recall_chunks_max_tokens == 333
|
||||
|
||||
|
||||
class TestMentalModelTriggerRecallFields:
|
||||
"""MentalModelTrigger Pydantic model accepts the new override fields."""
|
||||
|
||||
def test_trigger_accepts_new_fields(self):
|
||||
from hindsight_api.api.http import MentalModelTrigger
|
||||
|
||||
trigger = MentalModelTrigger(
|
||||
include_chunks=False,
|
||||
recall_max_tokens=512,
|
||||
recall_chunks_max_tokens=0,
|
||||
)
|
||||
assert trigger.include_chunks is False
|
||||
assert trigger.recall_max_tokens == 512
|
||||
assert trigger.recall_chunks_max_tokens == 0
|
||||
|
||||
def test_trigger_defaults_are_none(self):
|
||||
from hindsight_api.api.http import MentalModelTrigger
|
||||
|
||||
trigger = MentalModelTrigger()
|
||||
assert trigger.include_chunks is None
|
||||
assert trigger.recall_max_tokens is None
|
||||
assert trigger.recall_chunks_max_tokens is None
|
||||
|
||||
|
||||
class TestRefreshTriggerWiring:
|
||||
"""Verify mental-model refresh forwards trigger overrides into reflect_async kwargs."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trigger_overrides_passed_to_reflect_async(self, mock_request_context):
|
||||
from hindsight_api.engine.memory_engine import MemoryEngine
|
||||
from hindsight_api.engine.response_models import ReflectResult
|
||||
|
||||
engine = MemoryEngine.__new__(MemoryEngine)
|
||||
|
||||
async def fake_get_mental_model(bank_id, mental_model_id, request_context):
|
||||
return {
|
||||
"id": mental_model_id,
|
||||
"source_query": "What do we know?",
|
||||
"tags": [],
|
||||
"trigger": {
|
||||
"include_chunks": False,
|
||||
"recall_max_tokens": 512,
|
||||
"recall_chunks_max_tokens": 0,
|
||||
"fact_types": ["world"],
|
||||
},
|
||||
}
|
||||
|
||||
captured = {}
|
||||
|
||||
async def fake_reflect_async(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return ReflectResult(text="ok", based_on={})
|
||||
|
||||
async def fake_update_mental_model(*args, **kwargs):
|
||||
return None
|
||||
|
||||
engine.get_mental_model = fake_get_mental_model
|
||||
engine.reflect_async = fake_reflect_async
|
||||
engine.update_mental_model = fake_update_mental_model
|
||||
engine._operation_validator = None
|
||||
engine._tenant_extension = None
|
||||
|
||||
await engine.refresh_mental_model(
|
||||
bank_id="bank-1",
|
||||
mental_model_id="mm-1",
|
||||
request_context=mock_request_context,
|
||||
)
|
||||
|
||||
assert captured["recall_include_chunks"] is False
|
||||
assert captured["recall_max_tokens_override"] == 512
|
||||
assert captured["recall_chunks_max_tokens_override"] == 0
|
||||
assert captured["fact_types"] == ["world"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_trigger_fields_pass_none(self, mock_request_context):
|
||||
from hindsight_api.engine.memory_engine import MemoryEngine
|
||||
from hindsight_api.engine.response_models import ReflectResult
|
||||
|
||||
engine = MemoryEngine.__new__(MemoryEngine)
|
||||
|
||||
async def fake_get_mental_model(bank_id, mental_model_id, request_context):
|
||||
return {"id": mental_model_id, "source_query": "q", "tags": [], "trigger": {}}
|
||||
|
||||
captured = {}
|
||||
|
||||
async def fake_reflect_async(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return ReflectResult(text="ok", based_on={})
|
||||
|
||||
async def fake_update_mental_model(*args, **kwargs):
|
||||
return None
|
||||
|
||||
engine.get_mental_model = fake_get_mental_model
|
||||
engine.reflect_async = fake_reflect_async
|
||||
engine.update_mental_model = fake_update_mental_model
|
||||
engine._operation_validator = None
|
||||
engine._tenant_extension = None
|
||||
|
||||
await engine.refresh_mental_model(
|
||||
bank_id="bank-1",
|
||||
mental_model_id="mm-1",
|
||||
request_context=mock_request_context,
|
||||
)
|
||||
|
||||
# When trigger fields are absent, None is forwarded so reflect_async falls back to bank/global config.
|
||||
assert captured["recall_include_chunks"] is None
|
||||
assert captured["recall_max_tokens_override"] is None
|
||||
assert captured["recall_chunks_max_tokens_override"] is None
|
||||
@@ -396,6 +396,88 @@ class TestReflectAgentMocked:
|
||||
# Verify recall was actually called (normalization worked)
|
||||
mock_functions["recall_fn"].assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_short_circuit_answer_is_capped_by_max_tokens(self, mock_llm, mock_functions):
|
||||
"""When the LLM short-circuits (returns text without calling a tool) and the text
|
||||
exceeds max_tokens, the agent must rewrite it through a capped call so the final
|
||||
user-visible answer respects the configured limit.
|
||||
"""
|
||||
# Build a long response that's well over the cap in cl100k_base tokens.
|
||||
long_answer = " ".join(
|
||||
[
|
||||
"This is a detailed paragraph about the team, their roles, and their recurring meetings."
|
||||
]
|
||||
* 80
|
||||
)
|
||||
# The short-circuit path: tool_calls empty, content populated.
|
||||
mock_llm.call_with_tools.return_value = LLMToolCallResult(
|
||||
tool_calls=[],
|
||||
content=long_answer,
|
||||
finish_reason="stop",
|
||||
input_tokens=10,
|
||||
output_tokens=500,
|
||||
)
|
||||
mock_llm.call = AsyncMock(
|
||||
return_value=(
|
||||
"Short rewritten answer.",
|
||||
TokenUsage(input_tokens=50, output_tokens=10, total_tokens=60),
|
||||
)
|
||||
)
|
||||
|
||||
cap = 50
|
||||
result = await run_reflect_agent(
|
||||
llm_config=mock_llm,
|
||||
bank_id="test-bank",
|
||||
query="test query",
|
||||
bank_profile={"name": "Test", "mission": "Testing"},
|
||||
max_tokens=cap,
|
||||
**mock_functions,
|
||||
)
|
||||
|
||||
# The rewrite call must have been made, and it must carry the cap.
|
||||
assert mock_llm.call.await_count == 1, (
|
||||
f"expected exactly one capped rewrite call, got {mock_llm.call.await_count}"
|
||||
)
|
||||
rewrite_kwargs = mock_llm.call.await_args.kwargs
|
||||
assert rewrite_kwargs.get("max_completion_tokens") == cap, (
|
||||
f"rewrite call should use max_completion_tokens={cap}, "
|
||||
f"got {rewrite_kwargs.get('max_completion_tokens')}"
|
||||
)
|
||||
|
||||
# The final answer is the rewritten text, not the oversized original.
|
||||
assert result.text == "Short rewritten answer."
|
||||
|
||||
# The trace records the rewrite step so we can see it was invoked.
|
||||
assert any(entry.scope == "final_rewrite" for entry in result.llm_trace), (
|
||||
f"llm_trace should include a final_rewrite entry, got {result.llm_trace}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_short_circuit_answer_under_cap_is_not_rewritten(self, mock_llm, mock_functions):
|
||||
"""If the short-circuit answer already fits within max_tokens, no extra rewrite
|
||||
call should happen — we don't want to pay for a second LLM call in the common case.
|
||||
"""
|
||||
short_answer = "Small answer that already fits."
|
||||
mock_llm.call_with_tools.return_value = LLMToolCallResult(
|
||||
tool_calls=[],
|
||||
content=short_answer,
|
||||
finish_reason="stop",
|
||||
input_tokens=10,
|
||||
output_tokens=8,
|
||||
)
|
||||
|
||||
result = await run_reflect_agent(
|
||||
llm_config=mock_llm,
|
||||
bank_id="test-bank",
|
||||
query="test query",
|
||||
bank_profile={"name": "Test", "mission": "Testing"},
|
||||
max_tokens=200,
|
||||
**mock_functions,
|
||||
)
|
||||
|
||||
assert result.text == short_answer
|
||||
mock_llm.call.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_iterations_reached(self, mock_llm, mock_functions):
|
||||
"""Test that agent stops after max iterations even with errors."""
|
||||
|
||||
@@ -378,6 +378,7 @@ async def test_mentioned_at_vs_occurred(memory, request_context):
|
||||
content="Alice graduated from MIT in March 2020.",
|
||||
context="education history",
|
||||
event_date=conversation_date, # When this conversation happened
|
||||
fact_type_override="world",
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
@@ -1149,6 +1150,7 @@ async def test_chunk_fact_mapping(memory, request_context):
|
||||
content=content,
|
||||
context="technical documentation",
|
||||
document_id=document_id,
|
||||
fact_type_override="world",
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Unit tests for retain orchestrator mapping and embeddings length guarantee.
|
||||
|
||||
Regression coverage for issue #1037: a silent length mismatch between the
|
||||
extracted facts and the generated embeddings caused
|
||||
`_map_results_to_contents` to raise IndexError during batch_retain.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api.engine.retain import embedding_utils
|
||||
from hindsight_api.engine.retain.orchestrator import _map_results_to_contents
|
||||
from hindsight_api.engine.retain.types import ProcessedFact, RetainContent
|
||||
|
||||
|
||||
def _make_processed_fact(content_index: int, text: str = "fact") -> ProcessedFact:
|
||||
return ProcessedFact(
|
||||
fact_text=text,
|
||||
fact_type="world",
|
||||
embedding=[0.0, 0.0, 0.0],
|
||||
occurred_start=None,
|
||||
occurred_end=None,
|
||||
mentioned_at=datetime(2026, 1, 1),
|
||||
context="",
|
||||
metadata={},
|
||||
content_index=content_index,
|
||||
)
|
||||
|
||||
|
||||
def _make_content(text: str = "x") -> RetainContent:
|
||||
return RetainContent(content=text)
|
||||
|
||||
|
||||
class TestMapResultsToContents:
|
||||
def test_groups_unit_ids_by_content_index(self):
|
||||
contents = [_make_content("a"), _make_content("b"), _make_content("c")]
|
||||
processed = [
|
||||
_make_processed_fact(0, "a1"),
|
||||
_make_processed_fact(0, "a2"),
|
||||
_make_processed_fact(2, "c1"),
|
||||
]
|
||||
unit_ids = ["u-a1", "u-a2", "u-c1"]
|
||||
|
||||
result = _map_results_to_contents(contents, processed, unit_ids)
|
||||
|
||||
assert result == [["u-a1", "u-a2"], [], ["u-c1"]]
|
||||
|
||||
def test_handles_out_of_range_content_index(self):
|
||||
contents = [_make_content("a"), _make_content("b")]
|
||||
processed = [
|
||||
_make_processed_fact(-1, "f1"),
|
||||
_make_processed_fact(99, "f2"),
|
||||
]
|
||||
unit_ids = ["u1", "u2"]
|
||||
|
||||
result = _map_results_to_contents(contents, processed, unit_ids)
|
||||
|
||||
assert result == [["u1"], ["u2"]]
|
||||
|
||||
def test_empty_inputs(self):
|
||||
assert _map_results_to_contents([], [], []) == []
|
||||
|
||||
def test_length_mismatch_raises(self):
|
||||
# Regression for #1037: previously the function silently overran unit_ids.
|
||||
contents = [_make_content("a")]
|
||||
processed = [_make_processed_fact(0), _make_processed_fact(0)]
|
||||
unit_ids = ["u1"] # one fewer than processed_facts
|
||||
|
||||
with pytest.raises(ValueError, match="length mismatch"):
|
||||
_map_results_to_contents(contents, processed, unit_ids)
|
||||
|
||||
def test_unit_ids_assigned_by_processed_fact_position(self):
|
||||
# Even if processed_facts are interleaved across contents, each unit_id
|
||||
# must follow its corresponding processed_fact (positional alignment).
|
||||
contents = [_make_content("a"), _make_content("b")]
|
||||
processed = [
|
||||
_make_processed_fact(1, "b1"),
|
||||
_make_processed_fact(0, "a1"),
|
||||
_make_processed_fact(1, "b2"),
|
||||
]
|
||||
unit_ids = ["u-b1", "u-a1", "u-b2"]
|
||||
|
||||
result = _map_results_to_contents(contents, processed, unit_ids)
|
||||
|
||||
assert result == [["u-a1"], ["u-b1", "u-b2"]]
|
||||
|
||||
|
||||
class TestEmbeddingsBatchLengthGuarantee:
|
||||
def test_raises_when_backend_returns_fewer_embeddings(self):
|
||||
# Regression for #1037: backends that silently truncate must not pass
|
||||
# through — `zip(extracted_facts, embeddings)` would otherwise drop
|
||||
# facts and break unit_id alignment downstream.
|
||||
backend = MagicMock()
|
||||
backend.encode.return_value = [[0.1, 0.2]] # only 1 vector for 3 inputs
|
||||
|
||||
with pytest.raises(RuntimeError, match="returned 1 vectors for 3 input texts"):
|
||||
asyncio.run(embedding_utils.generate_embeddings_batch(backend, ["a", "b", "c"]))
|
||||
|
||||
def test_raises_when_backend_returns_more_embeddings(self):
|
||||
backend = MagicMock()
|
||||
backend.encode.return_value = [[0.1], [0.2], [0.3]]
|
||||
|
||||
with pytest.raises(RuntimeError, match="returned 3 vectors for 2 input texts"):
|
||||
asyncio.run(embedding_utils.generate_embeddings_batch(backend, ["a", "b"]))
|
||||
|
||||
def test_passes_through_aligned_embeddings(self):
|
||||
backend = MagicMock()
|
||||
backend.encode.return_value = [[0.1], [0.2]]
|
||||
|
||||
result = asyncio.run(embedding_utils.generate_embeddings_batch(backend, ["a", "b"]))
|
||||
|
||||
assert result == [[0.1], [0.2]]
|
||||
@@ -1505,8 +1505,7 @@ async def test_consolidation_slots_reserved_when_retain_saturates(pool, clean_op
|
||||
consolidation_started = [op for op, t in started.items() if t == "consolidation"]
|
||||
|
||||
assert len(retain_started) == 3, (
|
||||
f"Retain should be capped at max_slots - consolidation_max_slots = 3, "
|
||||
f"got {len(retain_started)}"
|
||||
f"Retain should be capped at max_slots - consolidation_max_slots = 3, got {len(retain_started)}"
|
||||
)
|
||||
assert len(consolidation_started) == 1, (
|
||||
f"Consolidation should claim its reserved slot even while retain saturates, "
|
||||
@@ -1529,6 +1528,96 @@ async def test_consolidation_slots_reserved_when_retain_saturates(pool, clean_op
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pending_breakdown_explains_unclaimable_rows(pool, clean_operations, caplog):
|
||||
"""Pending rows that the claim query filters out must be visible in logs.
|
||||
|
||||
Background: production incident where a 'pending' retain sat in the queue for
|
||||
hours while workers had free slots. With only the global pending count in
|
||||
[WORKER_STATS] there's no way to tell whether the rows are claimable-but-not-
|
||||
being-claimed (real bug) vs filtered out by the claim WHERE clause (data
|
||||
state). This test verifies [PENDING_BREAKDOWN] surfaces each filter bucket
|
||||
so operators can diagnose without DB access.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from hindsight_api.worker.poller import WorkerPoller
|
||||
|
||||
poller = WorkerPoller(
|
||||
pool=pool,
|
||||
worker_id="test-worker-pending-breakdown",
|
||||
executor=lambda _t: asyncio.sleep(0),
|
||||
poll_interval_ms=50,
|
||||
max_slots=5,
|
||||
)
|
||||
|
||||
bank_id = f"test-worker-{uuid.uuid4().hex[:8]}"
|
||||
await _ensure_bank(pool, bank_id)
|
||||
|
||||
# Mix of pending rows that the claim query treats differently:
|
||||
# * payload_null - batch_retain parent (orphan candidate)
|
||||
# * retry_blocked - failed once, scheduled an hour out
|
||||
# * assigned - worker_id stamped (e.g. left over from a prior crash
|
||||
# that re-queued without clearing worker_id)
|
||||
# * claimable - normal retain ready to go
|
||||
# * consolidation - normal consolidation, also claimable
|
||||
rows = [
|
||||
("batch_retain", None, None, None), # payload_null
|
||||
("retain", json.dumps({"type": "test"}), "future", None), # retry_blocked
|
||||
("retain", json.dumps({"type": "test"}), None, "ghost-worker"), # assigned
|
||||
("retain", json.dumps({"type": "test"}), None, None), # claimable
|
||||
("consolidation", json.dumps({"type": "test"}), None, None), # claimable
|
||||
]
|
||||
for op_type, payload, retry_marker, worker_id in rows:
|
||||
op_id = uuid.uuid4()
|
||||
await pool.execute(
|
||||
"""
|
||||
INSERT INTO async_operations
|
||||
(operation_id, bank_id, operation_type, status, task_payload,
|
||||
next_retry_at, worker_id)
|
||||
VALUES ($1, $2, $3, 'pending', $4::jsonb,
|
||||
CASE WHEN $5::text = 'future' THEN now() + interval '1 hour' ELSE NULL END,
|
||||
$6)
|
||||
""",
|
||||
op_id,
|
||||
bank_id,
|
||||
op_type,
|
||||
payload,
|
||||
retry_marker,
|
||||
worker_id,
|
||||
)
|
||||
|
||||
# Trigger one stats emit. _last_progress_log starts at 0, so the first call
|
||||
# always logs.
|
||||
with caplog.at_level(logging.INFO, logger="hindsight_api.worker.poller"):
|
||||
await poller._log_progress_if_due()
|
||||
|
||||
breakdown_lines = [r.message for r in caplog.records if r.message.startswith("[PENDING_BREAKDOWN]")]
|
||||
assert len(breakdown_lines) == 1, f"Expected exactly one breakdown line, got: {breakdown_lines}"
|
||||
|
||||
# The breakdown is global (not bank-scoped), so other rows in the table may
|
||||
# contribute. Parse the per-op_type buckets from the line and assert that
|
||||
# our additions appear (>= 1 for each bucket we populated).
|
||||
line = breakdown_lines[0]
|
||||
buckets: dict[str, dict[str, int]] = {}
|
||||
for section in line.removeprefix("[PENDING_BREAKDOWN]").split("|"):
|
||||
section = section.strip()
|
||||
if ":" not in section:
|
||||
continue
|
||||
op_type, fields = section.split(":", 1)
|
||||
kv = {}
|
||||
for token in fields.strip().split():
|
||||
k, _, v = token.partition("=")
|
||||
kv[k] = int(v)
|
||||
buckets[op_type.strip()] = kv
|
||||
|
||||
assert buckets["batch_retain"]["payload_null"] >= 1
|
||||
assert buckets["retain"]["retry_blocked"] >= 1
|
||||
assert buckets["retain"]["assigned"] >= 1
|
||||
assert buckets["retain"]["claimable"] >= 1
|
||||
assert buckets["consolidation"]["claimable"] >= 1
|
||||
|
||||
|
||||
class TestMarkFailedParentPropagation:
|
||||
"""Tests for _mark_failed parent propagation in WorkerPoller.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hindsight-api"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
description = "Hindsight: Agent Memory That Works Like Human Memory"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
# Operation-level skips
|
||||
# ---------------------------------------------------------------------------
|
||||
[skip]
|
||||
# (empty — every operation is currently wired)
|
||||
# UI-only endpoint powering the control-plane stats chart.
|
||||
# Zero-filled bucket arrays don't map to a useful CLI command.
|
||||
get_memories_timeseries = "UI-only endpoint for the control plane stats chart"
|
||||
|
||||
# UI-only endpoint powering the control-plane entity constellation view.
|
||||
# Returns nodes/edges in cytoscape shape; not a useful CLI command.
|
||||
get_entity_graph = "UI-only endpoint for the control plane entity constellation"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-operation parameter skips
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hindsight-cli"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
edition = "2021"
|
||||
authors = ["Hindsight Team"]
|
||||
description = "A beautiful CLI for Hindsight - semantic memory system"
|
||||
|
||||
@@ -735,7 +735,7 @@ impl ApiClient {
|
||||
self.runtime.block_on(async {
|
||||
let response = self
|
||||
.client
|
||||
.get_operation_status(bank_id, operation_id, None)
|
||||
.get_operation_status(bank_id, operation_id, None, None)
|
||||
.await?;
|
||||
Ok(response.into_inner())
|
||||
})
|
||||
|
||||
@@ -124,6 +124,9 @@ pub fn create(
|
||||
fact_types: None,
|
||||
tag_groups: None,
|
||||
tags_match: None,
|
||||
include_chunks: None,
|
||||
recall_max_tokens: None,
|
||||
recall_chunks_max_tokens: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -198,6 +201,9 @@ pub fn update(
|
||||
fact_types: None,
|
||||
tag_groups: None,
|
||||
tags_match: None,
|
||||
include_chunks: None,
|
||||
recall_max_tokens: None,
|
||||
recall_chunks_max_tokens: None,
|
||||
});
|
||||
|
||||
let request = types::UpdateMentalModelRequest {
|
||||
|
||||
@@ -7,7 +7,7 @@ info:
|
||||
name: Apache 2.0
|
||||
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
title: Hindsight HTTP API
|
||||
version: 0.5.1
|
||||
version: 0.5.2
|
||||
servers:
|
||||
- url: /
|
||||
paths:
|
||||
@@ -464,6 +464,53 @@ paths:
|
||||
summary: Get statistics for memory bank
|
||||
tags:
|
||||
- Banks
|
||||
/v1/default/banks/{bank_id}/stats/memories-timeseries:
|
||||
get:
|
||||
description: "Memories ingested over a period, bucketed by time and broken down\
|
||||
\ by fact type."
|
||||
operationId: get_memories_timeseries
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: true
|
||||
in: query
|
||||
name: period
|
||||
required: false
|
||||
schema:
|
||||
default: 7d
|
||||
title: Period
|
||||
type: string
|
||||
style: form
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MemoriesTimeseriesResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Memory ingestion time-series
|
||||
tags:
|
||||
- Banks
|
||||
/v1/default/banks/{bank_id}/entities:
|
||||
get:
|
||||
description: "List all entities (people, organizations, etc.) known by the bank,\
|
||||
@@ -524,6 +571,66 @@ paths:
|
||||
summary: List entities
|
||||
tags:
|
||||
- Entities
|
||||
/v1/default/banks/{bank_id}/entities/graph:
|
||||
get:
|
||||
description: Return a graph of entities (nodes) and their co-occurrences (edges)
|
||||
for visualization.
|
||||
operationId: get_entity_graph
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- description: Maximum number of co-occurrence edges to return
|
||||
explode: true
|
||||
in: query
|
||||
name: limit
|
||||
required: false
|
||||
schema:
|
||||
default: 1000
|
||||
description: Maximum number of co-occurrence edges to return
|
||||
title: Limit
|
||||
type: integer
|
||||
style: form
|
||||
- description: Minimum cooccurrence_count to include an edge
|
||||
explode: true
|
||||
in: query
|
||||
name: min_count
|
||||
required: false
|
||||
schema:
|
||||
default: 1
|
||||
description: Minimum cooccurrence_count to include an edge
|
||||
title: Min Count
|
||||
type: integer
|
||||
style: form
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EntityGraphResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Get entity co-occurrence graph
|
||||
tags:
|
||||
- Entities
|
||||
/v1/default/banks/{bank_id}/entities/{entity_id}:
|
||||
get:
|
||||
description: Get detailed information about an entity including observations
|
||||
@@ -1781,6 +1888,19 @@ paths:
|
||||
title: Operation Id
|
||||
type: string
|
||||
style: simple
|
||||
- description: Include the raw task payload (submission params) in the response.
|
||||
May be large.
|
||||
explode: true
|
||||
in: query
|
||||
name: include_payload
|
||||
required: false
|
||||
schema:
|
||||
default: false
|
||||
description: Include the raw task payload (submission params) in the response.
|
||||
May be large.
|
||||
title: Include Payload
|
||||
type: boolean
|
||||
style: form
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
@@ -3500,6 +3620,12 @@ components:
|
||||
failed_operations:
|
||||
title: Failed Operations
|
||||
type: integer
|
||||
operations_by_status:
|
||||
additionalProperties:
|
||||
type: integer
|
||||
description: "Async operations grouped by status (pending, in_progress,\
|
||||
\ completed, failed, cancelled)."
|
||||
title: Operations By Status
|
||||
last_consolidated_at:
|
||||
nullable: true
|
||||
type: string
|
||||
@@ -3576,6 +3702,39 @@ components:
|
||||
entities_allow_free_form:
|
||||
nullable: true
|
||||
type: boolean
|
||||
retain_default_strategy:
|
||||
nullable: true
|
||||
type: string
|
||||
retain_strategies:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
retain_chunk_batch_size:
|
||||
nullable: true
|
||||
type: integer
|
||||
mcp_enabled_tools:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
consolidation_llm_batch_size:
|
||||
nullable: true
|
||||
type: integer
|
||||
consolidation_source_facts_max_tokens:
|
||||
nullable: true
|
||||
type: integer
|
||||
consolidation_source_facts_max_tokens_per_observation:
|
||||
nullable: true
|
||||
type: integer
|
||||
max_observations_per_scope:
|
||||
nullable: true
|
||||
type: integer
|
||||
reflect_source_facts_max_tokens:
|
||||
nullable: true
|
||||
type: integer
|
||||
llm_gemini_safety_settings:
|
||||
items: {}
|
||||
nullable: true
|
||||
type: array
|
||||
title: BankTemplateConfig
|
||||
BankTemplateDirective:
|
||||
description: |-
|
||||
@@ -4381,6 +4540,58 @@ components:
|
||||
- mention_count
|
||||
- observations
|
||||
title: EntityDetailResponse
|
||||
EntityGraphResponse:
|
||||
description: Response model for entity co-occurrence graph endpoint.
|
||||
example:
|
||||
edges:
|
||||
- data:
|
||||
color: '#ffd700'
|
||||
id: uuid-1-uuid-2
|
||||
lastCooccurred: 2024-02-01T14:00:00Z
|
||||
lineStyle: solid
|
||||
linkType: cooccurrence
|
||||
source: uuid-1
|
||||
target: uuid-2
|
||||
weight: 5
|
||||
limit: 1000
|
||||
nodes:
|
||||
- data:
|
||||
color: '#42a5f5'
|
||||
id: uuid-1
|
||||
label: Alice
|
||||
mentionCount: 12
|
||||
- data:
|
||||
color: '#42a5f5'
|
||||
id: uuid-2
|
||||
label: Google
|
||||
mentionCount: 8
|
||||
total_edges: 1
|
||||
total_entities: 2
|
||||
properties:
|
||||
nodes:
|
||||
items:
|
||||
additionalProperties: {}
|
||||
type: array
|
||||
edges:
|
||||
items:
|
||||
additionalProperties: {}
|
||||
type: array
|
||||
total_entities:
|
||||
title: Total Entities
|
||||
type: integer
|
||||
total_edges:
|
||||
title: Total Edges
|
||||
type: integer
|
||||
limit:
|
||||
title: Limit
|
||||
type: integer
|
||||
required:
|
||||
- edges
|
||||
- limit
|
||||
- nodes
|
||||
- total_edges
|
||||
- total_entities
|
||||
title: EntityGraphResponse
|
||||
EntityIncludeOptions:
|
||||
description: Options for including entity observations in recall results.
|
||||
properties:
|
||||
@@ -4736,6 +4947,44 @@ components:
|
||||
- offset
|
||||
- total
|
||||
title: ListTagsResponse
|
||||
MemoriesTimeseriesResponse:
|
||||
description: Time-series of memory ingestion bucketed by time and fact type.
|
||||
example:
|
||||
period: period
|
||||
trunc: trunc
|
||||
bank_id: bank_id
|
||||
buckets:
|
||||
- world: 0
|
||||
observation: 1
|
||||
time: time
|
||||
experience: 6
|
||||
- world: 0
|
||||
observation: 1
|
||||
time: time
|
||||
experience: 6
|
||||
properties:
|
||||
bank_id:
|
||||
title: Bank Id
|
||||
type: string
|
||||
period:
|
||||
description: "One of: 1h, 12h, 1d, 7d, 30d, 90d."
|
||||
title: Period
|
||||
type: string
|
||||
trunc:
|
||||
description: "Bucket granularity: minute, hour, day."
|
||||
title: Trunc
|
||||
type: string
|
||||
buckets:
|
||||
description: "Per-bucket counts, always returned fully padded for the requested\
|
||||
\ period."
|
||||
items:
|
||||
$ref: '#/components/schemas/MemoryTimeseriesBucket'
|
||||
type: array
|
||||
required:
|
||||
- bank_id
|
||||
- period
|
||||
- trunc
|
||||
title: MemoriesTimeseriesResponse
|
||||
MemoryItem:
|
||||
description: Single memory item for retain.
|
||||
example:
|
||||
@@ -4793,6 +5042,36 @@ components:
|
||||
required:
|
||||
- content
|
||||
title: MemoryItem
|
||||
MemoryTimeseriesBucket:
|
||||
description: One bucket in the memory ingestion time-series.
|
||||
example:
|
||||
world: 0
|
||||
observation: 1
|
||||
time: time
|
||||
experience: 6
|
||||
properties:
|
||||
time:
|
||||
description: Bucket start timestamp in ISO-8601 (UTC).
|
||||
title: Time
|
||||
type: string
|
||||
world:
|
||||
default: 0
|
||||
description: World-fact memories ingested in this bucket.
|
||||
title: World
|
||||
type: integer
|
||||
experience:
|
||||
default: 0
|
||||
description: Experience memories ingested in this bucket.
|
||||
title: Experience
|
||||
type: integer
|
||||
observation:
|
||||
default: 0
|
||||
description: Observations recorded in this bucket.
|
||||
title: Observation
|
||||
type: integer
|
||||
required:
|
||||
- time
|
||||
title: MemoryTimeseriesBucket
|
||||
MentalModelListResponse:
|
||||
description: Response model for listing mental models.
|
||||
example:
|
||||
@@ -4807,6 +5086,7 @@ components:
|
||||
id: id
|
||||
trigger:
|
||||
refresh_after_consolidation: false
|
||||
recall_chunks_max_tokens: 1
|
||||
tag_groups:
|
||||
- match: any_strict
|
||||
tags:
|
||||
@@ -4822,8 +5102,10 @@ components:
|
||||
exclude_mental_model_ids:
|
||||
- exclude_mental_model_ids
|
||||
- exclude_mental_model_ids
|
||||
include_chunks: true
|
||||
tags_match: any
|
||||
exclude_mental_models: false
|
||||
recall_max_tokens: 6
|
||||
last_refreshed_at: last_refreshed_at
|
||||
content: content
|
||||
tags:
|
||||
@@ -4839,6 +5121,7 @@ components:
|
||||
id: id
|
||||
trigger:
|
||||
refresh_after_consolidation: false
|
||||
recall_chunks_max_tokens: 1
|
||||
tag_groups:
|
||||
- match: any_strict
|
||||
tags:
|
||||
@@ -4854,8 +5137,10 @@ components:
|
||||
exclude_mental_model_ids:
|
||||
- exclude_mental_model_ids
|
||||
- exclude_mental_model_ids
|
||||
include_chunks: true
|
||||
tags_match: any
|
||||
exclude_mental_models: false
|
||||
recall_max_tokens: 6
|
||||
last_refreshed_at: last_refreshed_at
|
||||
content: content
|
||||
tags:
|
||||
@@ -4882,6 +5167,7 @@ components:
|
||||
id: id
|
||||
trigger:
|
||||
refresh_after_consolidation: false
|
||||
recall_chunks_max_tokens: 1
|
||||
tag_groups:
|
||||
- match: any_strict
|
||||
tags:
|
||||
@@ -4897,8 +5183,10 @@ components:
|
||||
exclude_mental_model_ids:
|
||||
- exclude_mental_model_ids
|
||||
- exclude_mental_model_ids
|
||||
include_chunks: true
|
||||
tags_match: any
|
||||
exclude_mental_models: false
|
||||
recall_max_tokens: 6
|
||||
last_refreshed_at: last_refreshed_at
|
||||
content: content
|
||||
tags:
|
||||
@@ -4986,11 +5274,21 @@ components:
|
||||
$ref: '#/components/schemas/MentalModelTrigger_Input_tag_groups_inner'
|
||||
nullable: true
|
||||
type: array
|
||||
include_chunks:
|
||||
nullable: true
|
||||
type: boolean
|
||||
recall_max_tokens:
|
||||
nullable: true
|
||||
type: integer
|
||||
recall_chunks_max_tokens:
|
||||
nullable: true
|
||||
type: integer
|
||||
title: MentalModelTrigger
|
||||
MentalModelTrigger-Output:
|
||||
description: Trigger settings for a mental model.
|
||||
example:
|
||||
refresh_after_consolidation: false
|
||||
recall_chunks_max_tokens: 1
|
||||
tag_groups:
|
||||
- match: any_strict
|
||||
tags:
|
||||
@@ -5006,8 +5304,10 @@ components:
|
||||
exclude_mental_model_ids:
|
||||
- exclude_mental_model_ids
|
||||
- exclude_mental_model_ids
|
||||
include_chunks: true
|
||||
tags_match: any
|
||||
exclude_mental_models: false
|
||||
recall_max_tokens: 6
|
||||
properties:
|
||||
refresh_after_consolidation:
|
||||
default: false
|
||||
@@ -5048,6 +5348,15 @@ components:
|
||||
$ref: '#/components/schemas/MentalModelTrigger_Output_tag_groups_inner'
|
||||
nullable: true
|
||||
type: array
|
||||
include_chunks:
|
||||
nullable: true
|
||||
type: boolean
|
||||
recall_max_tokens:
|
||||
nullable: true
|
||||
type: integer
|
||||
recall_chunks_max_tokens:
|
||||
nullable: true
|
||||
type: integer
|
||||
title: MentalModelTrigger
|
||||
OperationResponse:
|
||||
description: Response model for a single async operation.
|
||||
@@ -5131,6 +5440,9 @@ components:
|
||||
$ref: '#/components/schemas/ChildOperationStatus'
|
||||
nullable: true
|
||||
type: array
|
||||
task_payload:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
required:
|
||||
- operation_id
|
||||
- status
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
@@ -905,6 +905,140 @@ func (a *BanksAPIService) GetBankProfileExecute(r ApiGetBankProfileRequest) (*Ba
|
||||
return localVarReturnValue, localVarHTTPResponse, nil
|
||||
}
|
||||
|
||||
type ApiGetMemoriesTimeseriesRequest struct {
|
||||
ctx context.Context
|
||||
ApiService *BanksAPIService
|
||||
bankId string
|
||||
period *string
|
||||
authorization *string
|
||||
}
|
||||
|
||||
func (r ApiGetMemoriesTimeseriesRequest) Period(period string) ApiGetMemoriesTimeseriesRequest {
|
||||
r.period = &period
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ApiGetMemoriesTimeseriesRequest) Authorization(authorization string) ApiGetMemoriesTimeseriesRequest {
|
||||
r.authorization = &authorization
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ApiGetMemoriesTimeseriesRequest) Execute() (*MemoriesTimeseriesResponse, *http.Response, error) {
|
||||
return r.ApiService.GetMemoriesTimeseriesExecute(r)
|
||||
}
|
||||
|
||||
/*
|
||||
GetMemoriesTimeseries Memory ingestion time-series
|
||||
|
||||
Memories ingested over a period, bucketed by time and broken down by fact type.
|
||||
|
||||
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||
@param bankId
|
||||
@return ApiGetMemoriesTimeseriesRequest
|
||||
*/
|
||||
func (a *BanksAPIService) GetMemoriesTimeseries(ctx context.Context, bankId string) ApiGetMemoriesTimeseriesRequest {
|
||||
return ApiGetMemoriesTimeseriesRequest{
|
||||
ApiService: a,
|
||||
ctx: ctx,
|
||||
bankId: bankId,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute executes the request
|
||||
// @return MemoriesTimeseriesResponse
|
||||
func (a *BanksAPIService) GetMemoriesTimeseriesExecute(r ApiGetMemoriesTimeseriesRequest) (*MemoriesTimeseriesResponse, *http.Response, error) {
|
||||
var (
|
||||
localVarHTTPMethod = http.MethodGet
|
||||
localVarPostBody interface{}
|
||||
formFiles []formFile
|
||||
localVarReturnValue *MemoriesTimeseriesResponse
|
||||
)
|
||||
|
||||
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "BanksAPIService.GetMemoriesTimeseries")
|
||||
if err != nil {
|
||||
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||
}
|
||||
|
||||
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/stats/memories-timeseries"
|
||||
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
|
||||
|
||||
localVarHeaderParams := make(map[string]string)
|
||||
localVarQueryParams := url.Values{}
|
||||
localVarFormParams := url.Values{}
|
||||
|
||||
if r.period != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "period", r.period, "form", "")
|
||||
} else {
|
||||
var defaultValue string = "7d"
|
||||
r.period = &defaultValue
|
||||
}
|
||||
// to determine the Content-Type header
|
||||
localVarHTTPContentTypes := []string{}
|
||||
|
||||
// set Content-Type header
|
||||
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||
if localVarHTTPContentType != "" {
|
||||
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||
}
|
||||
|
||||
// to determine the Accept header
|
||||
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||
|
||||
// set Accept header
|
||||
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||
if localVarHTTPHeaderAccept != "" {
|
||||
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||
}
|
||||
if r.authorization != nil {
|
||||
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
|
||||
}
|
||||
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||
if err != nil {
|
||||
return localVarReturnValue, nil, err
|
||||
}
|
||||
|
||||
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||
if err != nil || localVarHTTPResponse == nil {
|
||||
return localVarReturnValue, localVarHTTPResponse, err
|
||||
}
|
||||
|
||||
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||
localVarHTTPResponse.Body.Close()
|
||||
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||
if err != nil {
|
||||
return localVarReturnValue, localVarHTTPResponse, err
|
||||
}
|
||||
|
||||
if localVarHTTPResponse.StatusCode >= 300 {
|
||||
newErr := &GenericOpenAPIError{
|
||||
body: localVarBody,
|
||||
error: localVarHTTPResponse.Status,
|
||||
}
|
||||
if localVarHTTPResponse.StatusCode == 422 {
|
||||
var v HTTPValidationError
|
||||
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
newErr.error = err.Error()
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
|
||||
newErr.model = v
|
||||
}
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
|
||||
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
newErr := &GenericOpenAPIError{
|
||||
body: localVarBody,
|
||||
error: err.Error(),
|
||||
}
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
|
||||
return localVarReturnValue, localVarHTTPResponse, nil
|
||||
}
|
||||
|
||||
type ApiListBanksRequest struct {
|
||||
ctx context.Context
|
||||
ApiService *BanksAPIService
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
@@ -149,6 +149,154 @@ func (a *EntitiesAPIService) GetEntityExecute(r ApiGetEntityRequest) (*EntityDet
|
||||
return localVarReturnValue, localVarHTTPResponse, nil
|
||||
}
|
||||
|
||||
type ApiGetEntityGraphRequest struct {
|
||||
ctx context.Context
|
||||
ApiService *EntitiesAPIService
|
||||
bankId string
|
||||
limit *int32
|
||||
minCount *int32
|
||||
authorization *string
|
||||
}
|
||||
|
||||
// Maximum number of co-occurrence edges to return
|
||||
func (r ApiGetEntityGraphRequest) Limit(limit int32) ApiGetEntityGraphRequest {
|
||||
r.limit = &limit
|
||||
return r
|
||||
}
|
||||
|
||||
// Minimum cooccurrence_count to include an edge
|
||||
func (r ApiGetEntityGraphRequest) MinCount(minCount int32) ApiGetEntityGraphRequest {
|
||||
r.minCount = &minCount
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ApiGetEntityGraphRequest) Authorization(authorization string) ApiGetEntityGraphRequest {
|
||||
r.authorization = &authorization
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ApiGetEntityGraphRequest) Execute() (*EntityGraphResponse, *http.Response, error) {
|
||||
return r.ApiService.GetEntityGraphExecute(r)
|
||||
}
|
||||
|
||||
/*
|
||||
GetEntityGraph Get entity co-occurrence graph
|
||||
|
||||
Return a graph of entities (nodes) and their co-occurrences (edges) for visualization.
|
||||
|
||||
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||
@param bankId
|
||||
@return ApiGetEntityGraphRequest
|
||||
*/
|
||||
func (a *EntitiesAPIService) GetEntityGraph(ctx context.Context, bankId string) ApiGetEntityGraphRequest {
|
||||
return ApiGetEntityGraphRequest{
|
||||
ApiService: a,
|
||||
ctx: ctx,
|
||||
bankId: bankId,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute executes the request
|
||||
// @return EntityGraphResponse
|
||||
func (a *EntitiesAPIService) GetEntityGraphExecute(r ApiGetEntityGraphRequest) (*EntityGraphResponse, *http.Response, error) {
|
||||
var (
|
||||
localVarHTTPMethod = http.MethodGet
|
||||
localVarPostBody interface{}
|
||||
formFiles []formFile
|
||||
localVarReturnValue *EntityGraphResponse
|
||||
)
|
||||
|
||||
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "EntitiesAPIService.GetEntityGraph")
|
||||
if err != nil {
|
||||
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||
}
|
||||
|
||||
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/entities/graph"
|
||||
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
|
||||
|
||||
localVarHeaderParams := make(map[string]string)
|
||||
localVarQueryParams := url.Values{}
|
||||
localVarFormParams := url.Values{}
|
||||
|
||||
if r.limit != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "limit", r.limit, "form", "")
|
||||
} else {
|
||||
var defaultValue int32 = 1000
|
||||
r.limit = &defaultValue
|
||||
}
|
||||
if r.minCount != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "min_count", r.minCount, "form", "")
|
||||
} else {
|
||||
var defaultValue int32 = 1
|
||||
r.minCount = &defaultValue
|
||||
}
|
||||
// to determine the Content-Type header
|
||||
localVarHTTPContentTypes := []string{}
|
||||
|
||||
// set Content-Type header
|
||||
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||
if localVarHTTPContentType != "" {
|
||||
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||
}
|
||||
|
||||
// to determine the Accept header
|
||||
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||
|
||||
// set Accept header
|
||||
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||
if localVarHTTPHeaderAccept != "" {
|
||||
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||
}
|
||||
if r.authorization != nil {
|
||||
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
|
||||
}
|
||||
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||
if err != nil {
|
||||
return localVarReturnValue, nil, err
|
||||
}
|
||||
|
||||
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||
if err != nil || localVarHTTPResponse == nil {
|
||||
return localVarReturnValue, localVarHTTPResponse, err
|
||||
}
|
||||
|
||||
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||
localVarHTTPResponse.Body.Close()
|
||||
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||
if err != nil {
|
||||
return localVarReturnValue, localVarHTTPResponse, err
|
||||
}
|
||||
|
||||
if localVarHTTPResponse.StatusCode >= 300 {
|
||||
newErr := &GenericOpenAPIError{
|
||||
body: localVarBody,
|
||||
error: localVarHTTPResponse.Status,
|
||||
}
|
||||
if localVarHTTPResponse.StatusCode == 422 {
|
||||
var v HTTPValidationError
|
||||
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
newErr.error = err.Error()
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
|
||||
newErr.model = v
|
||||
}
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
|
||||
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
newErr := &GenericOpenAPIError{
|
||||
body: localVarBody,
|
||||
error: err.Error(),
|
||||
}
|
||||
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||
}
|
||||
|
||||
return localVarReturnValue, localVarHTTPResponse, nil
|
||||
}
|
||||
|
||||
type ApiListEntitiesRequest struct {
|
||||
ctx context.Context
|
||||
ApiService *EntitiesAPIService
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
@@ -154,9 +154,16 @@ type ApiGetOperationStatusRequest struct {
|
||||
ApiService *OperationsAPIService
|
||||
bankId string
|
||||
operationId string
|
||||
includePayload *bool
|
||||
authorization *string
|
||||
}
|
||||
|
||||
// Include the raw task payload (submission params) in the response. May be large.
|
||||
func (r ApiGetOperationStatusRequest) IncludePayload(includePayload bool) ApiGetOperationStatusRequest {
|
||||
r.includePayload = &includePayload
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ApiGetOperationStatusRequest) Authorization(authorization string) ApiGetOperationStatusRequest {
|
||||
r.authorization = &authorization
|
||||
return r
|
||||
@@ -208,6 +215,12 @@ func (a *OperationsAPIService) GetOperationStatusExecute(r ApiGetOperationStatus
|
||||
localVarQueryParams := url.Values{}
|
||||
localVarFormParams := url.Values{}
|
||||
|
||||
if r.includePayload != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "include_payload", r.includePayload, "form", "")
|
||||
} else {
|
||||
var defaultValue bool = false
|
||||
r.includePayload = &defaultValue
|
||||
}
|
||||
// to determine the Content-Type header
|
||||
localVarHTTPContentTypes := []string{}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
@@ -41,7 +41,7 @@ var (
|
||||
queryDescape = strings.NewReplacer( "%5B", "[", "%5D", "]" )
|
||||
)
|
||||
|
||||
// APIClient manages communication with the Hindsight HTTP API API v0.5.1
|
||||
// APIClient manages communication with the Hindsight HTTP API API v0.5.2
|
||||
// In most cases there should be only one, shared, APIClient.
|
||||
type APIClient struct {
|
||||
cfg *Configuration
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -2,9 +2,31 @@ package hindsight
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
// defaultUserAgent returns the User-Agent string sent on every request unless
|
||||
// the caller overrides cfg.UserAgent. The version is read from build info so
|
||||
// it stays in sync with the module version automatically; falls back to
|
||||
// "devel" when running from an unpinned local checkout.
|
||||
func defaultUserAgent() string {
|
||||
version := "devel"
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, dep := range info.Deps {
|
||||
if dep.Path == "github.com/vectorize-io/hindsight/hindsight-clients/go" {
|
||||
version = dep.Version
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return "hindsight-client-go/" + version
|
||||
}
|
||||
|
||||
// DefaultUserAgent is the User-Agent string sent on every request unless the
|
||||
// caller overrides cfg.UserAgent (e.g. for integrations identifying themselves).
|
||||
var DefaultUserAgent = defaultUserAgent()
|
||||
|
||||
// NewAPIClientWithToken creates a new API client configured with a base URL and API token.
|
||||
// The token is sent as a Bearer token in the Authorization header for all requests.
|
||||
// Note: this uses http.DefaultClient which has no timeout. Use NewAPIClientWithTimeout
|
||||
@@ -16,6 +38,7 @@ import (
|
||||
// resp, _, err := client.MemoryAPI.RetainMemories(ctx, bankID).RetainRequest(req).Execute()
|
||||
func NewAPIClientWithToken(baseURL, token string) *APIClient {
|
||||
cfg := NewConfiguration()
|
||||
cfg.UserAgent = DefaultUserAgent
|
||||
cfg.Servers = ServerConfigurations{
|
||||
{URL: baseURL},
|
||||
}
|
||||
@@ -32,6 +55,7 @@ func NewAPIClientWithToken(baseURL, token string) *APIClient {
|
||||
// resp, _, err := client.MemoryAPI.RetainMemories(ctx, bankID).RetainRequest(req).Execute()
|
||||
func NewAPIClientWithTimeout(baseURL, token string, timeout time.Duration) *APIClient {
|
||||
cfg := NewConfiguration()
|
||||
cfg.UserAgent = DefaultUserAgent
|
||||
cfg.Servers = ServerConfigurations{
|
||||
{URL: baseURL},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
@@ -31,6 +31,8 @@ type BankStatsResponse struct {
|
||||
LinksBreakdown map[string]map[string]int32 `json:"links_breakdown"`
|
||||
PendingOperations int32 `json:"pending_operations"`
|
||||
FailedOperations int32 `json:"failed_operations"`
|
||||
// Async operations grouped by status (pending, in_progress, completed, failed, cancelled).
|
||||
OperationsByStatus map[string]int32 `json:"operations_by_status,omitempty"`
|
||||
LastConsolidatedAt NullableString `json:"last_consolidated_at,omitempty"`
|
||||
// Number of memories not yet processed into observations
|
||||
PendingConsolidation *int32 `json:"pending_consolidation,omitempty"`
|
||||
@@ -315,6 +317,38 @@ func (o *BankStatsResponse) SetFailedOperations(v int32) {
|
||||
o.FailedOperations = v
|
||||
}
|
||||
|
||||
// GetOperationsByStatus returns the OperationsByStatus field value if set, zero value otherwise.
|
||||
func (o *BankStatsResponse) GetOperationsByStatus() map[string]int32 {
|
||||
if o == nil || IsNil(o.OperationsByStatus) {
|
||||
var ret map[string]int32
|
||||
return ret
|
||||
}
|
||||
return o.OperationsByStatus
|
||||
}
|
||||
|
||||
// GetOperationsByStatusOk returns a tuple with the OperationsByStatus field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *BankStatsResponse) GetOperationsByStatusOk() (map[string]int32, bool) {
|
||||
if o == nil || IsNil(o.OperationsByStatus) {
|
||||
return map[string]int32{}, false
|
||||
}
|
||||
return o.OperationsByStatus, true
|
||||
}
|
||||
|
||||
// HasOperationsByStatus returns a boolean if a field has been set.
|
||||
func (o *BankStatsResponse) HasOperationsByStatus() bool {
|
||||
if o != nil && !IsNil(o.OperationsByStatus) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetOperationsByStatus gets a reference to the given map[string]int32 and assigns it to the OperationsByStatus field.
|
||||
func (o *BankStatsResponse) SetOperationsByStatus(v map[string]int32) {
|
||||
o.OperationsByStatus = v
|
||||
}
|
||||
|
||||
// GetLastConsolidatedAt returns the LastConsolidatedAt field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankStatsResponse) GetLastConsolidatedAt() string {
|
||||
if o == nil || IsNil(o.LastConsolidatedAt.Get()) {
|
||||
@@ -441,6 +475,9 @@ func (o BankStatsResponse) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize["links_breakdown"] = o.LinksBreakdown
|
||||
toSerialize["pending_operations"] = o.PendingOperations
|
||||
toSerialize["failed_operations"] = o.FailedOperations
|
||||
if !IsNil(o.OperationsByStatus) {
|
||||
toSerialize["operations_by_status"] = o.OperationsByStatus
|
||||
}
|
||||
if o.LastConsolidatedAt.IsSet() {
|
||||
toSerialize["last_consolidated_at"] = o.LastConsolidatedAt.Get()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
@@ -31,6 +31,16 @@ type BankTemplateConfig struct {
|
||||
DispositionEmpathy NullableInt32 `json:"disposition_empathy,omitempty"`
|
||||
EntityLabels []map[string]interface{} `json:"entity_labels,omitempty"`
|
||||
EntitiesAllowFreeForm NullableBool `json:"entities_allow_free_form,omitempty"`
|
||||
RetainDefaultStrategy NullableString `json:"retain_default_strategy,omitempty"`
|
||||
RetainStrategies map[string]interface{} `json:"retain_strategies,omitempty"`
|
||||
RetainChunkBatchSize NullableInt32 `json:"retain_chunk_batch_size,omitempty"`
|
||||
McpEnabledTools []string `json:"mcp_enabled_tools,omitempty"`
|
||||
ConsolidationLlmBatchSize NullableInt32 `json:"consolidation_llm_batch_size,omitempty"`
|
||||
ConsolidationSourceFactsMaxTokens NullableInt32 `json:"consolidation_source_facts_max_tokens,omitempty"`
|
||||
ConsolidationSourceFactsMaxTokensPerObservation NullableInt32 `json:"consolidation_source_facts_max_tokens_per_observation,omitempty"`
|
||||
MaxObservationsPerScope NullableInt32 `json:"max_observations_per_scope,omitempty"`
|
||||
ReflectSourceFactsMaxTokens NullableInt32 `json:"reflect_source_facts_max_tokens,omitempty"`
|
||||
LlmGeminiSafetySettings []interface{} `json:"llm_gemini_safety_settings,omitempty"`
|
||||
}
|
||||
|
||||
// NewBankTemplateConfig instantiates a new BankTemplateConfig object
|
||||
@@ -545,6 +555,399 @@ func (o *BankTemplateConfig) UnsetEntitiesAllowFreeForm() {
|
||||
o.EntitiesAllowFreeForm.Unset()
|
||||
}
|
||||
|
||||
// GetRetainDefaultStrategy returns the RetainDefaultStrategy field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetRetainDefaultStrategy() string {
|
||||
if o == nil || IsNil(o.RetainDefaultStrategy.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.RetainDefaultStrategy.Get()
|
||||
}
|
||||
|
||||
// GetRetainDefaultStrategyOk returns a tuple with the RetainDefaultStrategy field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetRetainDefaultStrategyOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.RetainDefaultStrategy.Get(), o.RetainDefaultStrategy.IsSet()
|
||||
}
|
||||
|
||||
// HasRetainDefaultStrategy returns a boolean if a field has been set.
|
||||
func (o *BankTemplateConfig) HasRetainDefaultStrategy() bool {
|
||||
if o != nil && o.RetainDefaultStrategy.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetRetainDefaultStrategy gets a reference to the given NullableString and assigns it to the RetainDefaultStrategy field.
|
||||
func (o *BankTemplateConfig) SetRetainDefaultStrategy(v string) {
|
||||
o.RetainDefaultStrategy.Set(&v)
|
||||
}
|
||||
// SetRetainDefaultStrategyNil sets the value for RetainDefaultStrategy to be an explicit nil
|
||||
func (o *BankTemplateConfig) SetRetainDefaultStrategyNil() {
|
||||
o.RetainDefaultStrategy.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetRetainDefaultStrategy ensures that no value is present for RetainDefaultStrategy, not even an explicit nil
|
||||
func (o *BankTemplateConfig) UnsetRetainDefaultStrategy() {
|
||||
o.RetainDefaultStrategy.Unset()
|
||||
}
|
||||
|
||||
// GetRetainStrategies returns the RetainStrategies field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetRetainStrategies() map[string]interface{} {
|
||||
if o == nil {
|
||||
var ret map[string]interface{}
|
||||
return ret
|
||||
}
|
||||
return o.RetainStrategies
|
||||
}
|
||||
|
||||
// GetRetainStrategiesOk returns a tuple with the RetainStrategies field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetRetainStrategiesOk() (map[string]interface{}, bool) {
|
||||
if o == nil || IsNil(o.RetainStrategies) {
|
||||
return map[string]interface{}{}, false
|
||||
}
|
||||
return o.RetainStrategies, true
|
||||
}
|
||||
|
||||
// HasRetainStrategies returns a boolean if a field has been set.
|
||||
func (o *BankTemplateConfig) HasRetainStrategies() bool {
|
||||
if o != nil && !IsNil(o.RetainStrategies) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetRetainStrategies gets a reference to the given map[string]interface{} and assigns it to the RetainStrategies field.
|
||||
func (o *BankTemplateConfig) SetRetainStrategies(v map[string]interface{}) {
|
||||
o.RetainStrategies = v
|
||||
}
|
||||
|
||||
// GetRetainChunkBatchSize returns the RetainChunkBatchSize field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetRetainChunkBatchSize() int32 {
|
||||
if o == nil || IsNil(o.RetainChunkBatchSize.Get()) {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
return *o.RetainChunkBatchSize.Get()
|
||||
}
|
||||
|
||||
// GetRetainChunkBatchSizeOk returns a tuple with the RetainChunkBatchSize field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetRetainChunkBatchSizeOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.RetainChunkBatchSize.Get(), o.RetainChunkBatchSize.IsSet()
|
||||
}
|
||||
|
||||
// HasRetainChunkBatchSize returns a boolean if a field has been set.
|
||||
func (o *BankTemplateConfig) HasRetainChunkBatchSize() bool {
|
||||
if o != nil && o.RetainChunkBatchSize.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetRetainChunkBatchSize gets a reference to the given NullableInt32 and assigns it to the RetainChunkBatchSize field.
|
||||
func (o *BankTemplateConfig) SetRetainChunkBatchSize(v int32) {
|
||||
o.RetainChunkBatchSize.Set(&v)
|
||||
}
|
||||
// SetRetainChunkBatchSizeNil sets the value for RetainChunkBatchSize to be an explicit nil
|
||||
func (o *BankTemplateConfig) SetRetainChunkBatchSizeNil() {
|
||||
o.RetainChunkBatchSize.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetRetainChunkBatchSize ensures that no value is present for RetainChunkBatchSize, not even an explicit nil
|
||||
func (o *BankTemplateConfig) UnsetRetainChunkBatchSize() {
|
||||
o.RetainChunkBatchSize.Unset()
|
||||
}
|
||||
|
||||
// GetMcpEnabledTools returns the McpEnabledTools field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetMcpEnabledTools() []string {
|
||||
if o == nil {
|
||||
var ret []string
|
||||
return ret
|
||||
}
|
||||
return o.McpEnabledTools
|
||||
}
|
||||
|
||||
// GetMcpEnabledToolsOk returns a tuple with the McpEnabledTools field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetMcpEnabledToolsOk() ([]string, bool) {
|
||||
if o == nil || IsNil(o.McpEnabledTools) {
|
||||
return nil, false
|
||||
}
|
||||
return o.McpEnabledTools, true
|
||||
}
|
||||
|
||||
// HasMcpEnabledTools returns a boolean if a field has been set.
|
||||
func (o *BankTemplateConfig) HasMcpEnabledTools() bool {
|
||||
if o != nil && !IsNil(o.McpEnabledTools) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetMcpEnabledTools gets a reference to the given []string and assigns it to the McpEnabledTools field.
|
||||
func (o *BankTemplateConfig) SetMcpEnabledTools(v []string) {
|
||||
o.McpEnabledTools = v
|
||||
}
|
||||
|
||||
// GetConsolidationLlmBatchSize returns the ConsolidationLlmBatchSize field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetConsolidationLlmBatchSize() int32 {
|
||||
if o == nil || IsNil(o.ConsolidationLlmBatchSize.Get()) {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
return *o.ConsolidationLlmBatchSize.Get()
|
||||
}
|
||||
|
||||
// GetConsolidationLlmBatchSizeOk returns a tuple with the ConsolidationLlmBatchSize field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetConsolidationLlmBatchSizeOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.ConsolidationLlmBatchSize.Get(), o.ConsolidationLlmBatchSize.IsSet()
|
||||
}
|
||||
|
||||
// HasConsolidationLlmBatchSize returns a boolean if a field has been set.
|
||||
func (o *BankTemplateConfig) HasConsolidationLlmBatchSize() bool {
|
||||
if o != nil && o.ConsolidationLlmBatchSize.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetConsolidationLlmBatchSize gets a reference to the given NullableInt32 and assigns it to the ConsolidationLlmBatchSize field.
|
||||
func (o *BankTemplateConfig) SetConsolidationLlmBatchSize(v int32) {
|
||||
o.ConsolidationLlmBatchSize.Set(&v)
|
||||
}
|
||||
// SetConsolidationLlmBatchSizeNil sets the value for ConsolidationLlmBatchSize to be an explicit nil
|
||||
func (o *BankTemplateConfig) SetConsolidationLlmBatchSizeNil() {
|
||||
o.ConsolidationLlmBatchSize.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetConsolidationLlmBatchSize ensures that no value is present for ConsolidationLlmBatchSize, not even an explicit nil
|
||||
func (o *BankTemplateConfig) UnsetConsolidationLlmBatchSize() {
|
||||
o.ConsolidationLlmBatchSize.Unset()
|
||||
}
|
||||
|
||||
// GetConsolidationSourceFactsMaxTokens returns the ConsolidationSourceFactsMaxTokens field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetConsolidationSourceFactsMaxTokens() int32 {
|
||||
if o == nil || IsNil(o.ConsolidationSourceFactsMaxTokens.Get()) {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
return *o.ConsolidationSourceFactsMaxTokens.Get()
|
||||
}
|
||||
|
||||
// GetConsolidationSourceFactsMaxTokensOk returns a tuple with the ConsolidationSourceFactsMaxTokens field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetConsolidationSourceFactsMaxTokensOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.ConsolidationSourceFactsMaxTokens.Get(), o.ConsolidationSourceFactsMaxTokens.IsSet()
|
||||
}
|
||||
|
||||
// HasConsolidationSourceFactsMaxTokens returns a boolean if a field has been set.
|
||||
func (o *BankTemplateConfig) HasConsolidationSourceFactsMaxTokens() bool {
|
||||
if o != nil && o.ConsolidationSourceFactsMaxTokens.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetConsolidationSourceFactsMaxTokens gets a reference to the given NullableInt32 and assigns it to the ConsolidationSourceFactsMaxTokens field.
|
||||
func (o *BankTemplateConfig) SetConsolidationSourceFactsMaxTokens(v int32) {
|
||||
o.ConsolidationSourceFactsMaxTokens.Set(&v)
|
||||
}
|
||||
// SetConsolidationSourceFactsMaxTokensNil sets the value for ConsolidationSourceFactsMaxTokens to be an explicit nil
|
||||
func (o *BankTemplateConfig) SetConsolidationSourceFactsMaxTokensNil() {
|
||||
o.ConsolidationSourceFactsMaxTokens.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetConsolidationSourceFactsMaxTokens ensures that no value is present for ConsolidationSourceFactsMaxTokens, not even an explicit nil
|
||||
func (o *BankTemplateConfig) UnsetConsolidationSourceFactsMaxTokens() {
|
||||
o.ConsolidationSourceFactsMaxTokens.Unset()
|
||||
}
|
||||
|
||||
// GetConsolidationSourceFactsMaxTokensPerObservation returns the ConsolidationSourceFactsMaxTokensPerObservation field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetConsolidationSourceFactsMaxTokensPerObservation() int32 {
|
||||
if o == nil || IsNil(o.ConsolidationSourceFactsMaxTokensPerObservation.Get()) {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
return *o.ConsolidationSourceFactsMaxTokensPerObservation.Get()
|
||||
}
|
||||
|
||||
// GetConsolidationSourceFactsMaxTokensPerObservationOk returns a tuple with the ConsolidationSourceFactsMaxTokensPerObservation field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetConsolidationSourceFactsMaxTokensPerObservationOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.ConsolidationSourceFactsMaxTokensPerObservation.Get(), o.ConsolidationSourceFactsMaxTokensPerObservation.IsSet()
|
||||
}
|
||||
|
||||
// HasConsolidationSourceFactsMaxTokensPerObservation returns a boolean if a field has been set.
|
||||
func (o *BankTemplateConfig) HasConsolidationSourceFactsMaxTokensPerObservation() bool {
|
||||
if o != nil && o.ConsolidationSourceFactsMaxTokensPerObservation.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetConsolidationSourceFactsMaxTokensPerObservation gets a reference to the given NullableInt32 and assigns it to the ConsolidationSourceFactsMaxTokensPerObservation field.
|
||||
func (o *BankTemplateConfig) SetConsolidationSourceFactsMaxTokensPerObservation(v int32) {
|
||||
o.ConsolidationSourceFactsMaxTokensPerObservation.Set(&v)
|
||||
}
|
||||
// SetConsolidationSourceFactsMaxTokensPerObservationNil sets the value for ConsolidationSourceFactsMaxTokensPerObservation to be an explicit nil
|
||||
func (o *BankTemplateConfig) SetConsolidationSourceFactsMaxTokensPerObservationNil() {
|
||||
o.ConsolidationSourceFactsMaxTokensPerObservation.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetConsolidationSourceFactsMaxTokensPerObservation ensures that no value is present for ConsolidationSourceFactsMaxTokensPerObservation, not even an explicit nil
|
||||
func (o *BankTemplateConfig) UnsetConsolidationSourceFactsMaxTokensPerObservation() {
|
||||
o.ConsolidationSourceFactsMaxTokensPerObservation.Unset()
|
||||
}
|
||||
|
||||
// GetMaxObservationsPerScope returns the MaxObservationsPerScope field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetMaxObservationsPerScope() int32 {
|
||||
if o == nil || IsNil(o.MaxObservationsPerScope.Get()) {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
return *o.MaxObservationsPerScope.Get()
|
||||
}
|
||||
|
||||
// GetMaxObservationsPerScopeOk returns a tuple with the MaxObservationsPerScope field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetMaxObservationsPerScopeOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.MaxObservationsPerScope.Get(), o.MaxObservationsPerScope.IsSet()
|
||||
}
|
||||
|
||||
// HasMaxObservationsPerScope returns a boolean if a field has been set.
|
||||
func (o *BankTemplateConfig) HasMaxObservationsPerScope() bool {
|
||||
if o != nil && o.MaxObservationsPerScope.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetMaxObservationsPerScope gets a reference to the given NullableInt32 and assigns it to the MaxObservationsPerScope field.
|
||||
func (o *BankTemplateConfig) SetMaxObservationsPerScope(v int32) {
|
||||
o.MaxObservationsPerScope.Set(&v)
|
||||
}
|
||||
// SetMaxObservationsPerScopeNil sets the value for MaxObservationsPerScope to be an explicit nil
|
||||
func (o *BankTemplateConfig) SetMaxObservationsPerScopeNil() {
|
||||
o.MaxObservationsPerScope.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetMaxObservationsPerScope ensures that no value is present for MaxObservationsPerScope, not even an explicit nil
|
||||
func (o *BankTemplateConfig) UnsetMaxObservationsPerScope() {
|
||||
o.MaxObservationsPerScope.Unset()
|
||||
}
|
||||
|
||||
// GetReflectSourceFactsMaxTokens returns the ReflectSourceFactsMaxTokens field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetReflectSourceFactsMaxTokens() int32 {
|
||||
if o == nil || IsNil(o.ReflectSourceFactsMaxTokens.Get()) {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
return *o.ReflectSourceFactsMaxTokens.Get()
|
||||
}
|
||||
|
||||
// GetReflectSourceFactsMaxTokensOk returns a tuple with the ReflectSourceFactsMaxTokens field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetReflectSourceFactsMaxTokensOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.ReflectSourceFactsMaxTokens.Get(), o.ReflectSourceFactsMaxTokens.IsSet()
|
||||
}
|
||||
|
||||
// HasReflectSourceFactsMaxTokens returns a boolean if a field has been set.
|
||||
func (o *BankTemplateConfig) HasReflectSourceFactsMaxTokens() bool {
|
||||
if o != nil && o.ReflectSourceFactsMaxTokens.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetReflectSourceFactsMaxTokens gets a reference to the given NullableInt32 and assigns it to the ReflectSourceFactsMaxTokens field.
|
||||
func (o *BankTemplateConfig) SetReflectSourceFactsMaxTokens(v int32) {
|
||||
o.ReflectSourceFactsMaxTokens.Set(&v)
|
||||
}
|
||||
// SetReflectSourceFactsMaxTokensNil sets the value for ReflectSourceFactsMaxTokens to be an explicit nil
|
||||
func (o *BankTemplateConfig) SetReflectSourceFactsMaxTokensNil() {
|
||||
o.ReflectSourceFactsMaxTokens.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetReflectSourceFactsMaxTokens ensures that no value is present for ReflectSourceFactsMaxTokens, not even an explicit nil
|
||||
func (o *BankTemplateConfig) UnsetReflectSourceFactsMaxTokens() {
|
||||
o.ReflectSourceFactsMaxTokens.Unset()
|
||||
}
|
||||
|
||||
// GetLlmGeminiSafetySettings returns the LlmGeminiSafetySettings field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *BankTemplateConfig) GetLlmGeminiSafetySettings() []interface{} {
|
||||
if o == nil {
|
||||
var ret []interface{}
|
||||
return ret
|
||||
}
|
||||
return o.LlmGeminiSafetySettings
|
||||
}
|
||||
|
||||
// GetLlmGeminiSafetySettingsOk returns a tuple with the LlmGeminiSafetySettings field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *BankTemplateConfig) GetLlmGeminiSafetySettingsOk() ([]interface{}, bool) {
|
||||
if o == nil || IsNil(o.LlmGeminiSafetySettings) {
|
||||
return nil, false
|
||||
}
|
||||
return o.LlmGeminiSafetySettings, true
|
||||
}
|
||||
|
||||
// HasLlmGeminiSafetySettings returns a boolean if a field has been set.
|
||||
func (o *BankTemplateConfig) HasLlmGeminiSafetySettings() bool {
|
||||
if o != nil && !IsNil(o.LlmGeminiSafetySettings) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetLlmGeminiSafetySettings gets a reference to the given []interface{} and assigns it to the LlmGeminiSafetySettings field.
|
||||
func (o *BankTemplateConfig) SetLlmGeminiSafetySettings(v []interface{}) {
|
||||
o.LlmGeminiSafetySettings = v
|
||||
}
|
||||
|
||||
func (o BankTemplateConfig) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
@@ -591,6 +994,36 @@ func (o BankTemplateConfig) ToMap() (map[string]interface{}, error) {
|
||||
if o.EntitiesAllowFreeForm.IsSet() {
|
||||
toSerialize["entities_allow_free_form"] = o.EntitiesAllowFreeForm.Get()
|
||||
}
|
||||
if o.RetainDefaultStrategy.IsSet() {
|
||||
toSerialize["retain_default_strategy"] = o.RetainDefaultStrategy.Get()
|
||||
}
|
||||
if o.RetainStrategies != nil {
|
||||
toSerialize["retain_strategies"] = o.RetainStrategies
|
||||
}
|
||||
if o.RetainChunkBatchSize.IsSet() {
|
||||
toSerialize["retain_chunk_batch_size"] = o.RetainChunkBatchSize.Get()
|
||||
}
|
||||
if o.McpEnabledTools != nil {
|
||||
toSerialize["mcp_enabled_tools"] = o.McpEnabledTools
|
||||
}
|
||||
if o.ConsolidationLlmBatchSize.IsSet() {
|
||||
toSerialize["consolidation_llm_batch_size"] = o.ConsolidationLlmBatchSize.Get()
|
||||
}
|
||||
if o.ConsolidationSourceFactsMaxTokens.IsSet() {
|
||||
toSerialize["consolidation_source_facts_max_tokens"] = o.ConsolidationSourceFactsMaxTokens.Get()
|
||||
}
|
||||
if o.ConsolidationSourceFactsMaxTokensPerObservation.IsSet() {
|
||||
toSerialize["consolidation_source_facts_max_tokens_per_observation"] = o.ConsolidationSourceFactsMaxTokensPerObservation.Get()
|
||||
}
|
||||
if o.MaxObservationsPerScope.IsSet() {
|
||||
toSerialize["max_observations_per_scope"] = o.MaxObservationsPerScope.Get()
|
||||
}
|
||||
if o.ReflectSourceFactsMaxTokens.IsSet() {
|
||||
toSerialize["reflect_source_facts_max_tokens"] = o.ReflectSourceFactsMaxTokens.Get()
|
||||
}
|
||||
if o.LlmGeminiSafetySettings != nil {
|
||||
toSerialize["llm_gemini_safety_settings"] = o.LlmGeminiSafetySettings
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the EntityGraphResponse type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &EntityGraphResponse{}
|
||||
|
||||
// EntityGraphResponse Response model for entity co-occurrence graph endpoint.
|
||||
type EntityGraphResponse struct {
|
||||
Nodes []map[string]interface{} `json:"nodes"`
|
||||
Edges []map[string]interface{} `json:"edges"`
|
||||
TotalEntities int32 `json:"total_entities"`
|
||||
TotalEdges int32 `json:"total_edges"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
type _EntityGraphResponse EntityGraphResponse
|
||||
|
||||
// NewEntityGraphResponse instantiates a new EntityGraphResponse object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewEntityGraphResponse(nodes []map[string]interface{}, edges []map[string]interface{}, totalEntities int32, totalEdges int32, limit int32) *EntityGraphResponse {
|
||||
this := EntityGraphResponse{}
|
||||
this.Nodes = nodes
|
||||
this.Edges = edges
|
||||
this.TotalEntities = totalEntities
|
||||
this.TotalEdges = totalEdges
|
||||
this.Limit = limit
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewEntityGraphResponseWithDefaults instantiates a new EntityGraphResponse object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewEntityGraphResponseWithDefaults() *EntityGraphResponse {
|
||||
this := EntityGraphResponse{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetNodes returns the Nodes field value
|
||||
func (o *EntityGraphResponse) GetNodes() []map[string]interface{} {
|
||||
if o == nil {
|
||||
var ret []map[string]interface{}
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Nodes
|
||||
}
|
||||
|
||||
// GetNodesOk returns a tuple with the Nodes field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *EntityGraphResponse) GetNodesOk() ([]map[string]interface{}, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Nodes, true
|
||||
}
|
||||
|
||||
// SetNodes sets field value
|
||||
func (o *EntityGraphResponse) SetNodes(v []map[string]interface{}) {
|
||||
o.Nodes = v
|
||||
}
|
||||
|
||||
// GetEdges returns the Edges field value
|
||||
func (o *EntityGraphResponse) GetEdges() []map[string]interface{} {
|
||||
if o == nil {
|
||||
var ret []map[string]interface{}
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Edges
|
||||
}
|
||||
|
||||
// GetEdgesOk returns a tuple with the Edges field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *EntityGraphResponse) GetEdgesOk() ([]map[string]interface{}, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Edges, true
|
||||
}
|
||||
|
||||
// SetEdges sets field value
|
||||
func (o *EntityGraphResponse) SetEdges(v []map[string]interface{}) {
|
||||
o.Edges = v
|
||||
}
|
||||
|
||||
// GetTotalEntities returns the TotalEntities field value
|
||||
func (o *EntityGraphResponse) GetTotalEntities() int32 {
|
||||
if o == nil {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.TotalEntities
|
||||
}
|
||||
|
||||
// GetTotalEntitiesOk returns a tuple with the TotalEntities field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *EntityGraphResponse) GetTotalEntitiesOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.TotalEntities, true
|
||||
}
|
||||
|
||||
// SetTotalEntities sets field value
|
||||
func (o *EntityGraphResponse) SetTotalEntities(v int32) {
|
||||
o.TotalEntities = v
|
||||
}
|
||||
|
||||
// GetTotalEdges returns the TotalEdges field value
|
||||
func (o *EntityGraphResponse) GetTotalEdges() int32 {
|
||||
if o == nil {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.TotalEdges
|
||||
}
|
||||
|
||||
// GetTotalEdgesOk returns a tuple with the TotalEdges field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *EntityGraphResponse) GetTotalEdgesOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.TotalEdges, true
|
||||
}
|
||||
|
||||
// SetTotalEdges sets field value
|
||||
func (o *EntityGraphResponse) SetTotalEdges(v int32) {
|
||||
o.TotalEdges = v
|
||||
}
|
||||
|
||||
// GetLimit returns the Limit field value
|
||||
func (o *EntityGraphResponse) GetLimit() int32 {
|
||||
if o == nil {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Limit
|
||||
}
|
||||
|
||||
// GetLimitOk returns a tuple with the Limit field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *EntityGraphResponse) GetLimitOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Limit, true
|
||||
}
|
||||
|
||||
// SetLimit sets field value
|
||||
func (o *EntityGraphResponse) SetLimit(v int32) {
|
||||
o.Limit = v
|
||||
}
|
||||
|
||||
func (o EntityGraphResponse) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o EntityGraphResponse) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["nodes"] = o.Nodes
|
||||
toSerialize["edges"] = o.Edges
|
||||
toSerialize["total_entities"] = o.TotalEntities
|
||||
toSerialize["total_edges"] = o.TotalEdges
|
||||
toSerialize["limit"] = o.Limit
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *EntityGraphResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"nodes",
|
||||
"edges",
|
||||
"total_entities",
|
||||
"total_edges",
|
||||
"limit",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
for _, requiredProperty := range(requiredProperties) {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varEntityGraphResponse := _EntityGraphResponse{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varEntityGraphResponse)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = EntityGraphResponse(varEntityGraphResponse)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableEntityGraphResponse struct {
|
||||
value *EntityGraphResponse
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableEntityGraphResponse) Get() *EntityGraphResponse {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableEntityGraphResponse) Set(val *EntityGraphResponse) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableEntityGraphResponse) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableEntityGraphResponse) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableEntityGraphResponse(val *EntityGraphResponse) *NullableEntityGraphResponse {
|
||||
return &NullableEntityGraphResponse{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableEntityGraphResponse) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableEntityGraphResponse) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.5.1
|
||||
API version: 0.5.2
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user