feat: observation history tracking and diff UI (#513)
* feat: add source facts token limits to consolidation and recall
- Add two new configurable (per-bank) parameters:
- consolidation_source_facts_max_tokens: total token budget for source
facts across all observations in the consolidation prompt (-1 = unlimited)
- consolidation_source_facts_max_tokens_per_observation: per-observation
cap so each observation gets a fair share of source facts (-1 = unlimited,
default 256)
- Both are also exposed as recall API parameters via SourceFactsIncludeOptions
(max_tokens and max_tokens_per_observation)
- Consolidation now uses resolve_full_config to respect bank-level overrides
- Improve consolidation prompt: temporal metadata (occurred_start=, | Involving:)
is now clearly separated from observation text, with a concrete example showing
the expected synthesis style and explicit rules not to copy raw fact lines
- Add tests for recall source facts capping and consolidation config forwarding
- Expose all three new fields in the control plane bank config UI
- Document new env vars in configuration.md
- Regenerate OpenAPI spec and all SDK clients
* fix: reorder observations UI fields and rename Label Groups to Entity Labels
* fix: revert Entities section title (only rename inner label)
* doc: add consolidation source facts and batch size fields to memory-banks docs
* feat: add observation history tracking and UI diff view
- Track observation changes over time in a JSONB history column,
appending each update's previous state (text, tags, dates, sources)
instead of overwriting
- Add HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY config flag (default: true)
to toggle history recording
- Expose history field in get_memory_unit for observations
- Fix observations/[modelId] route that was proxying to wrong endpoint
- Add History tab in observation modal and History section in panel,
showing word-level and tag diffs between each change (newest first)
- Extract shared ObservationHistoryView component used by both modal and panel
- Add --random-port flag to start.sh to run multiple dev instances
- Scope Next.js distDir by port to prevent lock file collisions between instances
- Restyle consolidation pending badge (rounded-md with border) and add
inline refresh button; fix loading flicker on data refresh
* feat: dedicated observation history endpoint with source facts diff
- Add GET /memories/{id}/history endpoint returning enriched history with
resolved source fact texts and is_new flags per change
- Deprecate history field in GET /memories/{id} (always returns empty list)
- Reconstruct cumulative source facts per history entry by working backwards
from current state, marking newly added facts with is_new
- Replace inline history panel with "View History" button opening modal
- History modal fetches from dedicated endpoint lazily on tab switch
- Timeline view now opens MemoryDetailModal instead of side panel
- History view uses prev/next navigation (left = older, right = newer)
- Fix --random-port: pass dynamic API_PORT as HINDSIGHT_CP_DATAPLANE_API_URL
to control plane, preserving caller values over .env
This commit is contained in:
parent
99220d0527
commit
576473b6aa
23 changed files with 1459 additions and 216 deletions
|
|
@ -2137,7 +2137,7 @@ def _register_routes(app: FastAPI):
|
||||||
@app.get(
|
@app.get(
|
||||||
"/v1/default/banks/{bank_id}/memories/{memory_id}",
|
"/v1/default/banks/{bank_id}/memories/{memory_id}",
|
||||||
summary="Get memory unit",
|
summary="Get memory unit",
|
||||||
description="Get a single memory unit by ID with all its metadata including entities and tags.",
|
description="Get a single memory unit by ID with all its metadata including entities and tags. Note: the 'history' field is deprecated and always returns an empty list - use GET /memories/{memory_id}/history instead.",
|
||||||
operation_id="get_memory",
|
operation_id="get_memory",
|
||||||
tags=["Memory"],
|
tags=["Memory"],
|
||||||
)
|
)
|
||||||
|
|
@ -2167,6 +2167,39 @@ def _register_routes(app: FastAPI):
|
||||||
logger.error(f"Error in /v1/default/banks/{bank_id}/memories/{memory_id}: {error_detail}")
|
logger.error(f"Error in /v1/default/banks/{bank_id}/memories/{memory_id}: {error_detail}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/v1/default/banks/{bank_id}/memories/{memory_id}/history",
|
||||||
|
summary="Get observation history",
|
||||||
|
description="Get the full history of an observation, with each change's source facts resolved to their text.",
|
||||||
|
operation_id="get_observation_history",
|
||||||
|
tags=["Memory"],
|
||||||
|
)
|
||||||
|
async def api_get_observation_history(
|
||||||
|
bank_id: str,
|
||||||
|
memory_id: str,
|
||||||
|
request_context: RequestContext = Depends(get_request_context),
|
||||||
|
):
|
||||||
|
"""Get the history of a single observation by ID."""
|
||||||
|
try:
|
||||||
|
data = await app.state.memory.get_observation_history(
|
||||||
|
bank_id=bank_id,
|
||||||
|
memory_id=memory_id,
|
||||||
|
request_context=request_context,
|
||||||
|
)
|
||||||
|
if data is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Memory unit '{memory_id}' not found")
|
||||||
|
return 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}/memories/{memory_id}/history: {error_detail}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
"/v1/default/banks/{bank_id}/memories/recall",
|
"/v1/default/banks/{bank_id}/memories/recall",
|
||||||
response_model=RecallResponse,
|
response_model=RecallResponse,
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,7 @@ ENV_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION = (
|
||||||
"HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION"
|
"HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION"
|
||||||
)
|
)
|
||||||
ENV_OBSERVATIONS_MISSION = "HINDSIGHT_API_OBSERVATIONS_MISSION"
|
ENV_OBSERVATIONS_MISSION = "HINDSIGHT_API_OBSERVATIONS_MISSION"
|
||||||
|
ENV_ENABLE_OBSERVATION_HISTORY = "HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY"
|
||||||
|
|
||||||
# Webhook configuration (global, static - server-level only)
|
# Webhook configuration (global, static - server-level only)
|
||||||
ENV_WEBHOOK_URL = "HINDSIGHT_API_WEBHOOK_URL"
|
ENV_WEBHOOK_URL = "HINDSIGHT_API_WEBHOOK_URL"
|
||||||
|
|
@ -449,6 +450,7 @@ DEFAULT_FILE_DELETE_AFTER_RETAIN = True # Delete file bytes after retain (saves
|
||||||
|
|
||||||
# Observations defaults (consolidated knowledge from facts)
|
# Observations defaults (consolidated knowledge from facts)
|
||||||
DEFAULT_ENABLE_OBSERVATIONS = True # Observations enabled by default
|
DEFAULT_ENABLE_OBSERVATIONS = True # Observations enabled by default
|
||||||
|
DEFAULT_ENABLE_OBSERVATION_HISTORY = True # Observation history tracking enabled by default
|
||||||
DEFAULT_CONSOLIDATION_BATCH_SIZE = 50 # Memories to load per batch (internal memory optimization)
|
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_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
|
DEFAULT_CONSOLIDATION_MAX_TOKENS = 512 # Max tokens for recall when finding related observations
|
||||||
|
|
@ -727,6 +729,7 @@ class HindsightConfig:
|
||||||
|
|
||||||
# Observations settings (consolidated knowledge from facts)
|
# Observations settings (consolidated knowledge from facts)
|
||||||
enable_observations: bool
|
enable_observations: bool
|
||||||
|
enable_observation_history: bool
|
||||||
consolidation_batch_size: int
|
consolidation_batch_size: int
|
||||||
consolidation_llm_batch_size: int
|
consolidation_llm_batch_size: int
|
||||||
consolidation_max_tokens: int
|
consolidation_max_tokens: int
|
||||||
|
|
@ -1179,6 +1182,10 @@ class HindsightConfig:
|
||||||
== "true",
|
== "true",
|
||||||
# Observations settings (consolidated knowledge from facts)
|
# Observations settings (consolidated knowledge from facts)
|
||||||
enable_observations=os.getenv(ENV_ENABLE_OBSERVATIONS, str(DEFAULT_ENABLE_OBSERVATIONS)).lower() == "true",
|
enable_observations=os.getenv(ENV_ENABLE_OBSERVATIONS, str(DEFAULT_ENABLE_OBSERVATIONS)).lower() == "true",
|
||||||
|
enable_observation_history=os.getenv(
|
||||||
|
ENV_ENABLE_OBSERVATION_HISTORY, str(DEFAULT_ENABLE_OBSERVATION_HISTORY)
|
||||||
|
).lower()
|
||||||
|
== "true",
|
||||||
consolidation_batch_size=int(
|
consolidation_batch_size=int(
|
||||||
os.getenv(ENV_CONSOLIDATION_BATCH_SIZE, str(DEFAULT_CONSOLIDATION_BATCH_SIZE))
|
os.getenv(ENV_CONSOLIDATION_BATCH_SIZE, str(DEFAULT_CONSOLIDATION_BATCH_SIZE))
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -766,13 +766,17 @@ async def _execute_update_action(
|
||||||
logger.debug(f"Update skipped: observation {observation_id} not found in recall results")
|
logger.debug(f"Update skipped: observation {observation_id} not found in recall results")
|
||||||
return
|
return
|
||||||
|
|
||||||
history = [
|
from ...config import get_config
|
||||||
{
|
|
||||||
|
history_entry = {
|
||||||
"previous_text": model.text,
|
"previous_text": model.text,
|
||||||
|
"previous_tags": list(model.tags or []),
|
||||||
|
"previous_occurred_start": model.occurred_start,
|
||||||
|
"previous_occurred_end": model.occurred_end,
|
||||||
|
"previous_mentioned_at": model.mentioned_at,
|
||||||
"changed_at": datetime.now(timezone.utc).isoformat(),
|
"changed_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"source_memory_ids": [str(mid) for mid in source_memory_ids],
|
"new_source_memory_ids": [str(mid) for mid in source_memory_ids],
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
|
||||||
source_ids = list(model.source_fact_ids or []) + source_memory_ids
|
source_ids = list(model.source_fact_ids or []) + source_memory_ids
|
||||||
|
|
||||||
|
|
@ -787,13 +791,18 @@ async def _execute_update_action(
|
||||||
if perf:
|
if perf:
|
||||||
perf.record_timing("embedding", time.time() - t0)
|
perf.record_timing("embedding", time.time() - t0)
|
||||||
|
|
||||||
|
config = get_config()
|
||||||
|
history_clause = (
|
||||||
|
"history = COALESCE(history, '[]'::jsonb) || $3::jsonb," if config.enable_observation_history else ""
|
||||||
|
)
|
||||||
|
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
f"""
|
f"""
|
||||||
UPDATE {fq_table("memory_units")}
|
UPDATE {fq_table("memory_units")}
|
||||||
SET text = $1,
|
SET text = $1,
|
||||||
embedding = $2::vector,
|
embedding = $2::vector,
|
||||||
history = $3,
|
{history_clause}
|
||||||
source_memory_ids = $4,
|
source_memory_ids = $4,
|
||||||
proof_count = $5,
|
proof_count = $5,
|
||||||
tags = $10,
|
tags = $10,
|
||||||
|
|
@ -805,7 +814,7 @@ async def _execute_update_action(
|
||||||
""",
|
""",
|
||||||
new_text,
|
new_text,
|
||||||
embedding_str,
|
embedding_str,
|
||||||
json.dumps(history),
|
json.dumps([history_entry]),
|
||||||
source_ids,
|
source_ids,
|
||||||
len(source_ids),
|
len(source_ids),
|
||||||
uuid.UUID(observation_id),
|
uuid.UUID(observation_id),
|
||||||
|
|
|
||||||
|
|
@ -4345,7 +4345,11 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
"observation_scopes": row["observation_scopes"] if row["observation_scopes"] else None,
|
"observation_scopes": row["observation_scopes"] if row["observation_scopes"] else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
# For observations, include source_memory_ids and fetch source_memories
|
# For observations, include source_memory_ids
|
||||||
|
# history is deprecated here - use GET /memories/{id}/history instead
|
||||||
|
if row["fact_type"] == "observation":
|
||||||
|
result["history"] = []
|
||||||
|
|
||||||
if row["fact_type"] == "observation" and row["source_memory_ids"]:
|
if row["fact_type"] == "observation" and row["source_memory_ids"]:
|
||||||
source_ids = row["source_memory_ids"]
|
source_ids = row["source_memory_ids"]
|
||||||
result["source_memory_ids"] = [str(sid) for sid in source_ids]
|
result["source_memory_ids"] = [str(sid) for sid in source_ids]
|
||||||
|
|
@ -4374,6 +4378,95 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def get_observation_history(
|
||||||
|
self,
|
||||||
|
bank_id: str,
|
||||||
|
memory_id: str,
|
||||||
|
request_context: "RequestContext",
|
||||||
|
) -> list[dict] | None:
|
||||||
|
"""
|
||||||
|
Get the history of an observation, with source facts resolved to their text.
|
||||||
|
|
||||||
|
Returns None if the memory is not found or is not an observation.
|
||||||
|
Returns a list of history entries (most recent first), each with source_facts resolved.
|
||||||
|
"""
|
||||||
|
await self._authenticate_tenant(request_context)
|
||||||
|
if self._operation_validator:
|
||||||
|
from hindsight_api.extensions import BankReadContext
|
||||||
|
|
||||||
|
ctx = BankReadContext(bank_id=bank_id, operation="get_observation_history", 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:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
f"""
|
||||||
|
SELECT fact_type, history, source_memory_ids
|
||||||
|
FROM {fq_table("memory_units")}
|
||||||
|
WHERE id = $1 AND bank_id = $2
|
||||||
|
""",
|
||||||
|
uuid.UUID(memory_id),
|
||||||
|
bank_id,
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
if row["fact_type"] != "observation":
|
||||||
|
return []
|
||||||
|
|
||||||
|
raw_history = row["history"]
|
||||||
|
if isinstance(raw_history, str):
|
||||||
|
raw_history = json.loads(raw_history)
|
||||||
|
if not raw_history:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Collect all source memory IDs (current full set + all historical new ones)
|
||||||
|
current_source_ids: list[str] = [str(sid) for sid in (row["source_memory_ids"] or [])]
|
||||||
|
all_source_ids: set[uuid.UUID] = set(uuid.UUID(sid) for sid in current_source_ids)
|
||||||
|
for entry in raw_history:
|
||||||
|
for sid in entry.get("new_source_memory_ids", []):
|
||||||
|
try:
|
||||||
|
all_source_ids.add(uuid.UUID(sid))
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Resolve all source memories in one query
|
||||||
|
source_map: dict[str, dict] = {}
|
||||||
|
if all_source_ids:
|
||||||
|
source_rows = await conn.fetch(
|
||||||
|
f"""
|
||||||
|
SELECT id, text, fact_type, context
|
||||||
|
FROM {fq_table("memory_units")}
|
||||||
|
WHERE id = ANY($1::uuid[])
|
||||||
|
""",
|
||||||
|
list(all_source_ids),
|
||||||
|
)
|
||||||
|
for r in source_rows:
|
||||||
|
source_map[str(r["id"])] = {
|
||||||
|
"id": str(r["id"]),
|
||||||
|
"text": r["text"],
|
||||||
|
"type": r["fact_type"],
|
||||||
|
"context": r["context"] or None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reconstruct cumulative source IDs per change by working backwards from current state.
|
||||||
|
# Source IDs are only ever accumulated (never removed), so:
|
||||||
|
# after_change_N = before_change_N + new_source_memory_ids_N
|
||||||
|
cumulative_ids: list[str] = list(current_source_ids)
|
||||||
|
enriched: list[dict] = []
|
||||||
|
for entry in reversed(raw_history):
|
||||||
|
new_ids_in_entry: set[str] = set(entry.get("new_source_memory_ids", []))
|
||||||
|
source_facts = []
|
||||||
|
for sid in cumulative_ids:
|
||||||
|
fact = source_map.get(sid, {"id": sid, "text": None, "type": None, "context": None})
|
||||||
|
source_facts.append({**fact, "is_new": sid in new_ids_in_entry})
|
||||||
|
enriched_entry = dict(entry)
|
||||||
|
enriched_entry["source_facts"] = source_facts
|
||||||
|
enriched.append(enriched_entry)
|
||||||
|
# Step back: remove the new IDs added by this change to get the prior state
|
||||||
|
cumulative_ids = [sid for sid in cumulative_ids if sid not in new_ids_in_entry]
|
||||||
|
|
||||||
|
enriched.reverse()
|
||||||
|
return enriched
|
||||||
|
|
||||||
async def list_documents(
|
async def list_documents(
|
||||||
self,
|
self,
|
||||||
bank_id: str,
|
bank_id: str,
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,7 @@ def main():
|
||||||
enable_file_upload_api=config.enable_file_upload_api,
|
enable_file_upload_api=config.enable_file_upload_api,
|
||||||
file_delete_after_retain=config.file_delete_after_retain,
|
file_delete_after_retain=config.file_delete_after_retain,
|
||||||
enable_observations=config.enable_observations,
|
enable_observations=config.enable_observations,
|
||||||
|
enable_observation_history=config.enable_observation_history,
|
||||||
consolidation_batch_size=config.consolidation_batch_size,
|
consolidation_batch_size=config.consolidation_batch_size,
|
||||||
consolidation_llm_batch_size=config.consolidation_llm_batch_size,
|
consolidation_llm_batch_size=config.consolidation_llm_batch_size,
|
||||||
consolidation_max_tokens=config.consolidation_max_tokens,
|
consolidation_max_tokens=config.consolidation_max_tokens,
|
||||||
|
|
|
||||||
|
|
@ -210,8 +210,9 @@ paths:
|
||||||
- Memory
|
- Memory
|
||||||
/v1/default/banks/{bank_id}/memories/{memory_id}:
|
/v1/default/banks/{bank_id}/memories/{memory_id}:
|
||||||
get:
|
get:
|
||||||
description: Get a single memory unit by ID with all its metadata including
|
description: "Get a single memory unit by ID with all its metadata including\
|
||||||
entities and tags.
|
\ entities and tags. Note: the 'history' field is deprecated and always returns\
|
||||||
|
\ an empty list - use GET /memories/{memory_id}/history instead."
|
||||||
operationId: get_memory
|
operationId: get_memory
|
||||||
parameters:
|
parameters:
|
||||||
- explode: false
|
- explode: false
|
||||||
|
|
@ -253,6 +254,51 @@ paths:
|
||||||
summary: Get memory unit
|
summary: Get memory unit
|
||||||
tags:
|
tags:
|
||||||
- Memory
|
- Memory
|
||||||
|
/v1/default/banks/{bank_id}/memories/{memory_id}/history:
|
||||||
|
get:
|
||||||
|
description: "Get the full history of an observation, with each change's source\
|
||||||
|
\ facts resolved to their text."
|
||||||
|
operationId: get_observation_history
|
||||||
|
parameters:
|
||||||
|
- explode: false
|
||||||
|
in: path
|
||||||
|
name: bank_id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
title: Bank Id
|
||||||
|
type: string
|
||||||
|
style: simple
|
||||||
|
- explode: false
|
||||||
|
in: path
|
||||||
|
name: memory_id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
title: Memory Id
|
||||||
|
type: string
|
||||||
|
style: simple
|
||||||
|
- explode: false
|
||||||
|
in: header
|
||||||
|
name: authorization
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
style: simple
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
description: Successful Response
|
||||||
|
"422":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/HTTPValidationError'
|
||||||
|
description: Validation Error
|
||||||
|
summary: Get observation history
|
||||||
|
tags:
|
||||||
|
- Memory
|
||||||
/v1/default/banks/{bank_id}/memories/recall:
|
/v1/default/banks/{bank_id}/memories/recall:
|
||||||
post:
|
post:
|
||||||
description: |-
|
description: |-
|
||||||
|
|
|
||||||
|
|
@ -483,7 +483,7 @@ func (r ApiGetMemoryRequest) Execute() (interface{}, *http.Response, error) {
|
||||||
/*
|
/*
|
||||||
GetMemory Get memory unit
|
GetMemory Get memory unit
|
||||||
|
|
||||||
Get a single memory unit by ID with all its metadata including entities and tags.
|
Get a single memory unit by ID with all its metadata including entities and tags. Note: the 'history' field is deprecated and always returns an empty list - use GET /memories/{memory_id}/history instead.
|
||||||
|
|
||||||
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
@param bankId
|
@param bankId
|
||||||
|
|
@ -589,6 +589,132 @@ func (a *MemoryAPIService) GetMemoryExecute(r ApiGetMemoryRequest) (interface{},
|
||||||
return localVarReturnValue, localVarHTTPResponse, nil
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ApiGetObservationHistoryRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *MemoryAPIService
|
||||||
|
bankId string
|
||||||
|
memoryId string
|
||||||
|
authorization *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiGetObservationHistoryRequest) Authorization(authorization string) ApiGetObservationHistoryRequest {
|
||||||
|
r.authorization = &authorization
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiGetObservationHistoryRequest) Execute() (interface{}, *http.Response, error) {
|
||||||
|
return r.ApiService.GetObservationHistoryExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetObservationHistory Get observation history
|
||||||
|
|
||||||
|
Get the full history of an observation, with each change's source facts resolved to their text.
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@param bankId
|
||||||
|
@param memoryId
|
||||||
|
@return ApiGetObservationHistoryRequest
|
||||||
|
*/
|
||||||
|
func (a *MemoryAPIService) GetObservationHistory(ctx context.Context, bankId string, memoryId string) ApiGetObservationHistoryRequest {
|
||||||
|
return ApiGetObservationHistoryRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
bankId: bankId,
|
||||||
|
memoryId: memoryId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
// @return interface{}
|
||||||
|
func (a *MemoryAPIService) GetObservationHistoryExecute(r ApiGetObservationHistoryRequest) (interface{}, *http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodGet
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
localVarReturnValue interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MemoryAPIService.GetObservationHistory")
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/memories/{memory_id}/history"
|
||||||
|
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
|
||||||
|
localVarPath = strings.Replace(localVarPath, "{"+"memory_id"+"}", url.PathEscape(parameterValueToString(r.memoryId, "memoryId")), -1)
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
|
||||||
|
// 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 ApiListMemoriesRequest struct {
|
type ApiListMemoriesRequest struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
ApiService *MemoryAPIService
|
ApiService *MemoryAPIService
|
||||||
|
|
|
||||||
|
|
@ -1022,7 +1022,7 @@ class MemoryApi:
|
||||||
) -> object:
|
) -> object:
|
||||||
"""Get memory unit
|
"""Get memory unit
|
||||||
|
|
||||||
Get a single memory unit by ID with all its metadata including entities and tags.
|
Get a single memory unit by ID with all its metadata including entities and tags. Note: the 'history' field is deprecated and always returns an empty list - use GET /memories/{memory_id}/history instead.
|
||||||
|
|
||||||
:param bank_id: (required)
|
:param bank_id: (required)
|
||||||
:type bank_id: str
|
:type bank_id: str
|
||||||
|
|
@ -1098,7 +1098,7 @@ class MemoryApi:
|
||||||
) -> ApiResponse[object]:
|
) -> ApiResponse[object]:
|
||||||
"""Get memory unit
|
"""Get memory unit
|
||||||
|
|
||||||
Get a single memory unit by ID with all its metadata including entities and tags.
|
Get a single memory unit by ID with all its metadata including entities and tags. Note: the 'history' field is deprecated and always returns an empty list - use GET /memories/{memory_id}/history instead.
|
||||||
|
|
||||||
:param bank_id: (required)
|
:param bank_id: (required)
|
||||||
:type bank_id: str
|
:type bank_id: str
|
||||||
|
|
@ -1174,7 +1174,7 @@ class MemoryApi:
|
||||||
) -> RESTResponseType:
|
) -> RESTResponseType:
|
||||||
"""Get memory unit
|
"""Get memory unit
|
||||||
|
|
||||||
Get a single memory unit by ID with all its metadata including entities and tags.
|
Get a single memory unit by ID with all its metadata including entities and tags. Note: the 'history' field is deprecated and always returns an empty list - use GET /memories/{memory_id}/history instead.
|
||||||
|
|
||||||
:param bank_id: (required)
|
:param bank_id: (required)
|
||||||
:type bank_id: str
|
:type bank_id: str
|
||||||
|
|
@ -1294,6 +1294,299 @@ class MemoryApi:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@validate_call
|
||||||
|
async def get_observation_history(
|
||||||
|
self,
|
||||||
|
bank_id: StrictStr,
|
||||||
|
memory_id: StrictStr,
|
||||||
|
authorization: Optional[StrictStr] = None,
|
||||||
|
_request_timeout: Union[
|
||||||
|
None,
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Tuple[
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Annotated[StrictFloat, Field(gt=0)]
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
_request_auth: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_content_type: Optional[StrictStr] = None,
|
||||||
|
_headers: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
|
||||||
|
) -> object:
|
||||||
|
"""Get observation history
|
||||||
|
|
||||||
|
Get the full history of an observation, with each change's source facts resolved to their text.
|
||||||
|
|
||||||
|
:param bank_id: (required)
|
||||||
|
:type bank_id: str
|
||||||
|
:param memory_id: (required)
|
||||||
|
:type memory_id: str
|
||||||
|
:param authorization:
|
||||||
|
:type authorization: str
|
||||||
|
:param _request_timeout: timeout setting for this request. If one
|
||||||
|
number provided, it will be total request
|
||||||
|
timeout. It can also be a pair (tuple) of
|
||||||
|
(connection, read) timeouts.
|
||||||
|
:type _request_timeout: int, tuple(int, int), optional
|
||||||
|
:param _request_auth: set to override the auth_settings for an a single
|
||||||
|
request; this effectively ignores the
|
||||||
|
authentication in the spec for a single request.
|
||||||
|
:type _request_auth: dict, optional
|
||||||
|
:param _content_type: force content-type for the request.
|
||||||
|
:type _content_type: str, Optional
|
||||||
|
:param _headers: set to override the headers for a single
|
||||||
|
request; this effectively ignores the headers
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _headers: dict, optional
|
||||||
|
:param _host_index: set to override the host_index for a single
|
||||||
|
request; this effectively ignores the host_index
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _host_index: int, optional
|
||||||
|
:return: Returns the result object.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
_param = self._get_observation_history_serialize(
|
||||||
|
bank_id=bank_id,
|
||||||
|
memory_id=memory_id,
|
||||||
|
authorization=authorization,
|
||||||
|
_request_auth=_request_auth,
|
||||||
|
_content_type=_content_type,
|
||||||
|
_headers=_headers,
|
||||||
|
_host_index=_host_index
|
||||||
|
)
|
||||||
|
|
||||||
|
_response_types_map: Dict[str, Optional[str]] = {
|
||||||
|
'200': "object",
|
||||||
|
'422': "HTTPValidationError",
|
||||||
|
}
|
||||||
|
response_data = await self.api_client.call_api(
|
||||||
|
*_param,
|
||||||
|
_request_timeout=_request_timeout
|
||||||
|
)
|
||||||
|
await response_data.read()
|
||||||
|
return self.api_client.response_deserialize(
|
||||||
|
response_data=response_data,
|
||||||
|
response_types_map=_response_types_map,
|
||||||
|
).data
|
||||||
|
|
||||||
|
|
||||||
|
@validate_call
|
||||||
|
async def get_observation_history_with_http_info(
|
||||||
|
self,
|
||||||
|
bank_id: StrictStr,
|
||||||
|
memory_id: StrictStr,
|
||||||
|
authorization: Optional[StrictStr] = None,
|
||||||
|
_request_timeout: Union[
|
||||||
|
None,
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Tuple[
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Annotated[StrictFloat, Field(gt=0)]
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
_request_auth: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_content_type: Optional[StrictStr] = None,
|
||||||
|
_headers: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
|
||||||
|
) -> ApiResponse[object]:
|
||||||
|
"""Get observation history
|
||||||
|
|
||||||
|
Get the full history of an observation, with each change's source facts resolved to their text.
|
||||||
|
|
||||||
|
:param bank_id: (required)
|
||||||
|
:type bank_id: str
|
||||||
|
:param memory_id: (required)
|
||||||
|
:type memory_id: str
|
||||||
|
:param authorization:
|
||||||
|
:type authorization: str
|
||||||
|
:param _request_timeout: timeout setting for this request. If one
|
||||||
|
number provided, it will be total request
|
||||||
|
timeout. It can also be a pair (tuple) of
|
||||||
|
(connection, read) timeouts.
|
||||||
|
:type _request_timeout: int, tuple(int, int), optional
|
||||||
|
:param _request_auth: set to override the auth_settings for an a single
|
||||||
|
request; this effectively ignores the
|
||||||
|
authentication in the spec for a single request.
|
||||||
|
:type _request_auth: dict, optional
|
||||||
|
:param _content_type: force content-type for the request.
|
||||||
|
:type _content_type: str, Optional
|
||||||
|
:param _headers: set to override the headers for a single
|
||||||
|
request; this effectively ignores the headers
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _headers: dict, optional
|
||||||
|
:param _host_index: set to override the host_index for a single
|
||||||
|
request; this effectively ignores the host_index
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _host_index: int, optional
|
||||||
|
:return: Returns the result object.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
_param = self._get_observation_history_serialize(
|
||||||
|
bank_id=bank_id,
|
||||||
|
memory_id=memory_id,
|
||||||
|
authorization=authorization,
|
||||||
|
_request_auth=_request_auth,
|
||||||
|
_content_type=_content_type,
|
||||||
|
_headers=_headers,
|
||||||
|
_host_index=_host_index
|
||||||
|
)
|
||||||
|
|
||||||
|
_response_types_map: Dict[str, Optional[str]] = {
|
||||||
|
'200': "object",
|
||||||
|
'422': "HTTPValidationError",
|
||||||
|
}
|
||||||
|
response_data = await self.api_client.call_api(
|
||||||
|
*_param,
|
||||||
|
_request_timeout=_request_timeout
|
||||||
|
)
|
||||||
|
await response_data.read()
|
||||||
|
return self.api_client.response_deserialize(
|
||||||
|
response_data=response_data,
|
||||||
|
response_types_map=_response_types_map,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@validate_call
|
||||||
|
async def get_observation_history_without_preload_content(
|
||||||
|
self,
|
||||||
|
bank_id: StrictStr,
|
||||||
|
memory_id: StrictStr,
|
||||||
|
authorization: Optional[StrictStr] = None,
|
||||||
|
_request_timeout: Union[
|
||||||
|
None,
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Tuple[
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Annotated[StrictFloat, Field(gt=0)]
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
_request_auth: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_content_type: Optional[StrictStr] = None,
|
||||||
|
_headers: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
|
||||||
|
) -> RESTResponseType:
|
||||||
|
"""Get observation history
|
||||||
|
|
||||||
|
Get the full history of an observation, with each change's source facts resolved to their text.
|
||||||
|
|
||||||
|
:param bank_id: (required)
|
||||||
|
:type bank_id: str
|
||||||
|
:param memory_id: (required)
|
||||||
|
:type memory_id: str
|
||||||
|
:param authorization:
|
||||||
|
:type authorization: str
|
||||||
|
:param _request_timeout: timeout setting for this request. If one
|
||||||
|
number provided, it will be total request
|
||||||
|
timeout. It can also be a pair (tuple) of
|
||||||
|
(connection, read) timeouts.
|
||||||
|
:type _request_timeout: int, tuple(int, int), optional
|
||||||
|
:param _request_auth: set to override the auth_settings for an a single
|
||||||
|
request; this effectively ignores the
|
||||||
|
authentication in the spec for a single request.
|
||||||
|
:type _request_auth: dict, optional
|
||||||
|
:param _content_type: force content-type for the request.
|
||||||
|
:type _content_type: str, Optional
|
||||||
|
:param _headers: set to override the headers for a single
|
||||||
|
request; this effectively ignores the headers
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _headers: dict, optional
|
||||||
|
:param _host_index: set to override the host_index for a single
|
||||||
|
request; this effectively ignores the host_index
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _host_index: int, optional
|
||||||
|
:return: Returns the result object.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
_param = self._get_observation_history_serialize(
|
||||||
|
bank_id=bank_id,
|
||||||
|
memory_id=memory_id,
|
||||||
|
authorization=authorization,
|
||||||
|
_request_auth=_request_auth,
|
||||||
|
_content_type=_content_type,
|
||||||
|
_headers=_headers,
|
||||||
|
_host_index=_host_index
|
||||||
|
)
|
||||||
|
|
||||||
|
_response_types_map: Dict[str, Optional[str]] = {
|
||||||
|
'200': "object",
|
||||||
|
'422': "HTTPValidationError",
|
||||||
|
}
|
||||||
|
response_data = await self.api_client.call_api(
|
||||||
|
*_param,
|
||||||
|
_request_timeout=_request_timeout
|
||||||
|
)
|
||||||
|
return response_data.response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_observation_history_serialize(
|
||||||
|
self,
|
||||||
|
bank_id,
|
||||||
|
memory_id,
|
||||||
|
authorization,
|
||||||
|
_request_auth,
|
||||||
|
_content_type,
|
||||||
|
_headers,
|
||||||
|
_host_index,
|
||||||
|
) -> RequestSerialized:
|
||||||
|
|
||||||
|
_host = None
|
||||||
|
|
||||||
|
_collection_formats: Dict[str, str] = {
|
||||||
|
}
|
||||||
|
|
||||||
|
_path_params: Dict[str, str] = {}
|
||||||
|
_query_params: List[Tuple[str, str]] = []
|
||||||
|
_header_params: Dict[str, Optional[str]] = _headers or {}
|
||||||
|
_form_params: List[Tuple[str, str]] = []
|
||||||
|
_files: Dict[
|
||||||
|
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
|
||||||
|
] = {}
|
||||||
|
_body_params: Optional[bytes] = None
|
||||||
|
|
||||||
|
# process the path parameters
|
||||||
|
if bank_id is not None:
|
||||||
|
_path_params['bank_id'] = bank_id
|
||||||
|
if memory_id is not None:
|
||||||
|
_path_params['memory_id'] = memory_id
|
||||||
|
# process the query parameters
|
||||||
|
# process the header parameters
|
||||||
|
if authorization is not None:
|
||||||
|
_header_params['authorization'] = authorization
|
||||||
|
# process the form parameters
|
||||||
|
# process the body parameter
|
||||||
|
|
||||||
|
|
||||||
|
# set the HTTP header `Accept`
|
||||||
|
if 'Accept' not in _header_params:
|
||||||
|
_header_params['Accept'] = self.api_client.select_header_accept(
|
||||||
|
[
|
||||||
|
'application/json'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# authentication setting
|
||||||
|
_auth_settings: List[str] = [
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.api_client.param_serialize(
|
||||||
|
method='GET',
|
||||||
|
resource_path='/v1/default/banks/{bank_id}/memories/{memory_id}/history',
|
||||||
|
path_params=_path_params,
|
||||||
|
query_params=_query_params,
|
||||||
|
header_params=_header_params,
|
||||||
|
body=_body_params,
|
||||||
|
post_params=_form_params,
|
||||||
|
files=_files,
|
||||||
|
auth_settings=_auth_settings,
|
||||||
|
collection_formats=_collection_formats,
|
||||||
|
_host=_host,
|
||||||
|
_request_auth=_request_auth
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@validate_call
|
@validate_call
|
||||||
async def list_memories(
|
async def list_memories(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,9 @@ import type {
|
||||||
GetMentalModelData,
|
GetMentalModelData,
|
||||||
GetMentalModelErrors,
|
GetMentalModelErrors,
|
||||||
GetMentalModelResponses,
|
GetMentalModelResponses,
|
||||||
|
GetObservationHistoryData,
|
||||||
|
GetObservationHistoryErrors,
|
||||||
|
GetObservationHistoryResponses,
|
||||||
GetOperationStatusData,
|
GetOperationStatusData,
|
||||||
GetOperationStatusErrors,
|
GetOperationStatusErrors,
|
||||||
GetOperationStatusResponses,
|
GetOperationStatusResponses,
|
||||||
|
|
@ -252,7 +255,7 @@ export const listMemories = <ThrowOnError extends boolean = false>(
|
||||||
/**
|
/**
|
||||||
* Get memory unit
|
* Get memory unit
|
||||||
*
|
*
|
||||||
* Get a single memory unit by ID with all its metadata including entities and tags.
|
* Get a single memory unit by ID with all its metadata including entities and tags. Note: the 'history' field is deprecated and always returns an empty list - use GET /memories/{memory_id}/history instead.
|
||||||
*/
|
*/
|
||||||
export const getMemory = <ThrowOnError extends boolean = false>(
|
export const getMemory = <ThrowOnError extends boolean = false>(
|
||||||
options: Options<GetMemoryData, ThrowOnError>,
|
options: Options<GetMemoryData, ThrowOnError>,
|
||||||
|
|
@ -263,6 +266,23 @@ export const getMemory = <ThrowOnError extends boolean = false>(
|
||||||
ThrowOnError
|
ThrowOnError
|
||||||
>({ url: "/v1/default/banks/{bank_id}/memories/{memory_id}", ...options });
|
>({ url: "/v1/default/banks/{bank_id}/memories/{memory_id}", ...options });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get observation history
|
||||||
|
*
|
||||||
|
* Get the full history of an observation, with each change's source facts resolved to their text.
|
||||||
|
*/
|
||||||
|
export const getObservationHistory = <ThrowOnError extends boolean = false>(
|
||||||
|
options: Options<GetObservationHistoryData, ThrowOnError>,
|
||||||
|
) =>
|
||||||
|
(options.client ?? client).get<
|
||||||
|
GetObservationHistoryResponses,
|
||||||
|
GetObservationHistoryErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
url: "/v1/default/banks/{bank_id}/memories/{memory_id}/history",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recall memory
|
* Recall memory
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -2549,6 +2549,45 @@ export type GetMemoryResponses = {
|
||||||
200: unknown;
|
200: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetObservationHistoryData = {
|
||||||
|
body?: never;
|
||||||
|
headers?: {
|
||||||
|
/**
|
||||||
|
* Authorization
|
||||||
|
*/
|
||||||
|
authorization?: string | null;
|
||||||
|
};
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Bank Id
|
||||||
|
*/
|
||||||
|
bank_id: string;
|
||||||
|
/**
|
||||||
|
* Memory Id
|
||||||
|
*/
|
||||||
|
memory_id: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: "/v1/default/banks/{bank_id}/memories/{memory_id}/history";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetObservationHistoryErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetObservationHistoryError =
|
||||||
|
GetObservationHistoryErrors[keyof GetObservationHistoryErrors];
|
||||||
|
|
||||||
|
export type GetObservationHistoryResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
export type RecallMemoriesData = {
|
export type RecallMemoriesData = {
|
||||||
body: RecallRequest;
|
body: RecallRequest;
|
||||||
headers?: {
|
headers?: {
|
||||||
|
|
|
||||||
1
hindsight-control-plane/.gitignore
vendored
1
hindsight-control-plane/.gitignore
vendored
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
|
/.next-*/
|
||||||
/out/
|
/out/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,14 @@ import path from "path";
|
||||||
|
|
||||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
|
||||||
|
|
||||||
|
// Use a port-scoped distDir so multiple dev instances don't collide on the lock file
|
||||||
|
const distDir = process.env.PORT && process.env.PORT !== '9999'
|
||||||
|
? `.next-${process.env.PORT}`
|
||||||
|
: '.next';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
distDir,
|
||||||
basePath: basePath,
|
basePath: basePath,
|
||||||
assetPrefix: basePath,
|
assetPrefix: basePath,
|
||||||
// Disable request logging in production
|
// Disable request logging in production
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,14 @@ export async function GET(
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models/${modelId}`,
|
`${DATAPLANE_URL}/v1/default/banks/${bankId}/memories/${modelId}`,
|
||||||
{ method: "GET", headers: getDataplaneHeaders() }
|
{ method: "GET", headers: getDataplaneHeaders() }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.error("API error getting mental model:", errorText);
|
console.error("API error getting observation:", errorText);
|
||||||
return NextResponse.json(
|
return NextResponse.json({ error: "Failed to get observation" }, { status: response.status });
|
||||||
{ error: "Failed to get mental model" },
|
|
||||||
{ status: response.status }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { DATAPLANE_URL, getDataplaneHeaders } from "@/lib/hindsight-client";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ memoryId: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { memoryId } = await params;
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const bankId = searchParams.get("bank_id");
|
||||||
|
|
||||||
|
if (!bankId) {
|
||||||
|
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${DATAPLANE_URL}/v1/default/banks/${bankId}/memories/${memoryId}/history`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
return NextResponse.json({ error: "Memory not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
throw new Error(`API returned ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return NextResponse.json(data, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching observation history:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to fetch observation history" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -280,7 +280,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{loading ? (
|
{loading && !data ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<RefreshCw className="w-8 h-8 mx-auto mb-3 text-muted-foreground animate-spin" />
|
<RefreshCw className="w-8 h-8 mx-auto mb-3 text-muted-foreground animate-spin" />
|
||||||
<p className="text-muted-foreground">Loading memories...</p>
|
<p className="text-muted-foreground">Loading memories...</p>
|
||||||
|
|
@ -370,11 +370,11 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
|
|
||||||
{/* Consolidation status for observations */}
|
{/* Consolidation status for observations */}
|
||||||
{factType === "observation" && consolidationStatus && (
|
{factType === "observation" && consolidationStatus && (
|
||||||
<div
|
<span
|
||||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${
|
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium border ${
|
||||||
consolidationStatus.pending_consolidation === 0
|
consolidationStatus.pending_consolidation === 0
|
||||||
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20"
|
? "bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20"
|
||||||
: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20"
|
: "bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-500/20"
|
||||||
}`}
|
}`}
|
||||||
title={
|
title={
|
||||||
consolidationStatus.pending_consolidation === 0
|
consolidationStatus.pending_consolidation === 0
|
||||||
|
|
@ -391,9 +391,23 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
<>
|
<>
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
{consolidationStatus.pending_consolidation} Pending
|
{consolidationStatus.pending_consolidation} Pending
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
loadData(
|
||||||
|
fetchLimit,
|
||||||
|
searchQuery || undefined,
|
||||||
|
tagFilters.length > 0 ? tagFilters : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={loading}
|
||||||
|
className="ml-0.5 opacity-70 hover:opacity-100 disabled:opacity-40 transition-opacity"
|
||||||
|
title="Refresh observations"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-3 h-3 ${loading ? "animate-spin" : ""}`} />
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 bg-muted rounded-lg p-1">
|
<div className="flex items-center gap-2 bg-muted rounded-lg p-1">
|
||||||
|
|
@ -874,6 +888,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
data={data}
|
data={data}
|
||||||
filteredRows={filteredTableRows}
|
filteredRows={filteredTableRows}
|
||||||
bankId={currentBank || undefined}
|
bankId={currentBank || undefined}
|
||||||
|
onMemoryClick={(id) => setModalMemoryId(id)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -899,12 +914,13 @@ function TimelineView({
|
||||||
data,
|
data,
|
||||||
filteredRows,
|
filteredRows,
|
||||||
bankId,
|
bankId,
|
||||||
|
onMemoryClick,
|
||||||
}: {
|
}: {
|
||||||
data: any;
|
data: any;
|
||||||
filteredRows: any[];
|
filteredRows: any[];
|
||||||
bankId?: string;
|
bankId?: string;
|
||||||
|
onMemoryClick: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [selectedItem, setSelectedItem] = useState<any>(null);
|
|
||||||
const [granularity, setGranularity] = useState<Granularity>("month");
|
const [granularity, setGranularity] = useState<Granularity>("month");
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const timelineRef = useRef<HTMLDivElement>(null);
|
const timelineRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -1182,10 +1198,8 @@ function TimelineView({
|
||||||
{group.items.map((item: any, idx: number) => (
|
{group.items.map((item: any, idx: number) => (
|
||||||
<div
|
<div
|
||||||
key={item.id || idx}
|
key={item.id || idx}
|
||||||
onClick={() => setSelectedItem(item)}
|
onClick={() => onMemoryClick(item.id)}
|
||||||
className={`flex items-start cursor-pointer group ${
|
className={`flex items-start cursor-pointer group ${"hover:opacity-80"}`}
|
||||||
selectedItem?.id === item.id ? "opacity-100" : "hover:opacity-80"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{/* Date & Time */}
|
{/* Date & Time */}
|
||||||
<div className="w-[60px] text-right pr-3 pt-1 flex-shrink-0">
|
<div className="w-[60px] text-right pr-3 pt-1 flex-shrink-0">
|
||||||
|
|
@ -1200,21 +1214,13 @@ function TimelineView({
|
||||||
{/* Connector dot */}
|
{/* Connector dot */}
|
||||||
<div className="flex-shrink-0 pt-2">
|
<div className="flex-shrink-0 pt-2">
|
||||||
<div
|
<div
|
||||||
className={`w-1.5 h-1.5 rounded-full z-10 ${
|
className={`w-1.5 h-1.5 rounded-full z-10 ${"bg-muted-foreground/50 group-hover:bg-primary"}`}
|
||||||
selectedItem?.id === item.id
|
|
||||||
? "bg-primary"
|
|
||||||
: "bg-muted-foreground/50 group-hover:bg-primary"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card */}
|
{/* Card */}
|
||||||
<div
|
<div
|
||||||
className={`ml-3 flex-1 p-2 rounded border transition-colors ${
|
className={`ml-3 flex-1 p-2 rounded border transition-colors ${"bg-card border-border hover:border-primary/50"}`}
|
||||||
selectedItem?.id === item.id
|
|
||||||
? "bg-primary/10 border-primary"
|
|
||||||
: "bg-card border-border hover:border-primary/50"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<p className="text-xs text-foreground line-clamp-2 leading-relaxed">
|
<p className="text-xs text-foreground line-clamp-2 leading-relaxed">
|
||||||
{item.text}
|
{item.text}
|
||||||
|
|
@ -1252,18 +1258,6 @@ function TimelineView({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Detail Panel - Fixed on Right */}
|
|
||||||
{selectedItem && (
|
|
||||||
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l-2 border-primary shadow-2xl z-50 overflow-y-auto animate-in slide-in-from-right duration-300 ease-out">
|
|
||||||
<MemoryDetailPanel
|
|
||||||
memory={selectedItem}
|
|
||||||
onClose={() => setSelectedItem(null)}
|
|
||||||
inPanel
|
|
||||||
bankId={bankId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import { client } from "@/lib/api";
|
||||||
import { useBank } from "@/lib/bank-context";
|
import { useBank } from "@/lib/bank-context";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Loader2, Calendar, Users, FileText, Layers, Tag } from "lucide-react";
|
import { Loader2, Calendar, Users, FileText, Layers, Tag, History } from "lucide-react";
|
||||||
import { TagList } from "@/components/ui/tag-list";
|
import { TagList } from "@/components/ui/tag-list";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ObservationHistoryView, type HistoryEntry } from "@/components/observation-history-view";
|
||||||
|
|
||||||
interface SourceMemory {
|
interface SourceMemory {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -38,14 +39,15 @@ interface MemoryDetail {
|
||||||
interface MemoryDetailModalProps {
|
interface MemoryDetailModalProps {
|
||||||
memoryId: string | null;
|
memoryId: string | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
initialTab?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MemoryDetailModal({ memoryId, onClose }: MemoryDetailModalProps) {
|
export function MemoryDetailModal({ memoryId, onClose, initialTab }: MemoryDetailModalProps) {
|
||||||
const { currentBank } = useBank();
|
const { currentBank } = useBank();
|
||||||
const [memory, setMemory] = useState<MemoryDetail | null>(null);
|
const [memory, setMemory] = useState<MemoryDetail | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState("memory");
|
const [activeTab, setActiveTab] = useState(initialTab ?? "memory");
|
||||||
|
|
||||||
// Document and chunk data
|
// Document and chunk data
|
||||||
const [document, setDocument] = useState<any>(null);
|
const [document, setDocument] = useState<any>(null);
|
||||||
|
|
@ -53,6 +55,10 @@ export function MemoryDetailModal({ memoryId, onClose }: MemoryDetailModalProps)
|
||||||
const [loadingDocument, setLoadingDocument] = useState(false);
|
const [loadingDocument, setLoadingDocument] = useState(false);
|
||||||
const [loadingChunk, setLoadingChunk] = useState(false);
|
const [loadingChunk, setLoadingChunk] = useState(false);
|
||||||
|
|
||||||
|
// History data (fetched lazily from dedicated endpoint)
|
||||||
|
const [history, setHistory] = useState<HistoryEntry[] | null>(null);
|
||||||
|
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||||
|
|
||||||
// Source memory modal (for viewing source memories of observations)
|
// Source memory modal (for viewing source memories of observations)
|
||||||
const [sourceMemoryModalId, setSourceMemoryModalId] = useState<string | null>(null);
|
const [sourceMemoryModalId, setSourceMemoryModalId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -66,7 +72,8 @@ export function MemoryDetailModal({ memoryId, onClose }: MemoryDetailModalProps)
|
||||||
setMemory(null);
|
setMemory(null);
|
||||||
setDocument(null);
|
setDocument(null);
|
||||||
setChunk(null);
|
setChunk(null);
|
||||||
setActiveTab("memory");
|
setHistory(null);
|
||||||
|
setActiveTab(initialTab ?? "memory");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await client.getMemory(memoryId, currentBank);
|
const data = await client.getMemory(memoryId, currentBank);
|
||||||
|
|
@ -82,6 +89,33 @@ export function MemoryDetailModal({ memoryId, onClose }: MemoryDetailModalProps)
|
||||||
loadMemory();
|
loadMemory();
|
||||||
}, [memoryId, currentBank]);
|
}, [memoryId, currentBank]);
|
||||||
|
|
||||||
|
// Load history lazily when history tab is selected
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
activeTab !== "history" ||
|
||||||
|
!memory ||
|
||||||
|
memory.type !== "observation" ||
|
||||||
|
!currentBank ||
|
||||||
|
history !== null
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const loadHistory = async () => {
|
||||||
|
setLoadingHistory(true);
|
||||||
|
try {
|
||||||
|
const data = await client.getObservationHistory(memory.id, currentBank);
|
||||||
|
setHistory(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading history:", err);
|
||||||
|
setHistory([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingHistory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadHistory();
|
||||||
|
}, [activeTab, memory, currentBank, history]);
|
||||||
|
|
||||||
// Load document when tab is selected
|
// Load document when tab is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab !== "document" || !memory?.document_id || !currentBank || document) return;
|
if (activeTab !== "document" || !memory?.document_id || !currentBank || document) return;
|
||||||
|
|
@ -152,11 +186,31 @@ export function MemoryDetailModal({ memoryId, onClose }: MemoryDetailModalProps)
|
||||||
</div>
|
</div>
|
||||||
) : memory ? (
|
) : memory ? (
|
||||||
isObservation ? (
|
isObservation ? (
|
||||||
/* Observation view - no tabs since chunk/document don't apply */
|
/* Observation view - tabs for Info and History */
|
||||||
<div className="flex-1 overflow-y-auto space-y-4">
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="flex-1 flex flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="memory" className="flex items-center gap-1.5">
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
Observation
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="history" className="flex items-center gap-1.5">
|
||||||
|
<History className="w-3.5 h-3.5" />
|
||||||
|
History
|
||||||
|
{history && history.length > 0 ? ` (${history.length})` : ""}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto mt-4">
|
||||||
|
<TabsContent value="memory" className="mt-0 space-y-4">
|
||||||
{/* Text */}
|
{/* Text */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Text</div>
|
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
|
||||||
|
Text
|
||||||
|
</div>
|
||||||
<p className="text-sm text-foreground leading-relaxed">{memory.text}</p>
|
<p className="text-sm text-foreground leading-relaxed">{memory.text}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -170,7 +224,8 @@ export function MemoryDetailModal({ memoryId, onClose }: MemoryDetailModalProps)
|
||||||
<Calendar className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
<Calendar className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{new Date(memory.occurred_start).toLocaleString()}
|
{new Date(memory.occurred_start).toLocaleString()}
|
||||||
{memory.occurred_end && memory.occurred_end !== memory.occurred_start && (
|
{memory.occurred_end &&
|
||||||
|
memory.occurred_end !== memory.occurred_start && (
|
||||||
<>
|
<>
|
||||||
<span className="text-muted-foreground mx-1">→</span>
|
<span className="text-muted-foreground mx-1">→</span>
|
||||||
{new Date(memory.occurred_end).toLocaleString()}
|
{new Date(memory.occurred_end).toLocaleString()}
|
||||||
|
|
@ -307,7 +362,32 @@ export function MemoryDetailModal({ memoryId, onClose }: MemoryDetailModalProps)
|
||||||
{memory.id}
|
{memory.id}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="history" className="mt-0">
|
||||||
|
{loadingHistory ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
) : history && history.length > 0 ? (
|
||||||
|
<ObservationHistoryView
|
||||||
|
history={history}
|
||||||
|
current={{
|
||||||
|
text: memory.text,
|
||||||
|
tags: memory.tags,
|
||||||
|
occurred_start: memory.occurred_start,
|
||||||
|
occurred_end: memory.occurred_end,
|
||||||
|
mentioned_at: memory.mentioned_at,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground italic">
|
||||||
|
No history recorded yet.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
) : (
|
) : (
|
||||||
/* World/Experience view - with tabs */
|
/* World/Experience view - with tabs */
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { TagList } from "@/components/ui/tag-list";
|
import { TagList } from "@/components/ui/tag-list";
|
||||||
import { Copy, Check, X, Loader2, Calendar } from "lucide-react";
|
import { Copy, Check, X, Loader2, Calendar, History } from "lucide-react";
|
||||||
import { DocumentChunkModal } from "./document-chunk-modal";
|
import { DocumentChunkModal } from "./document-chunk-modal";
|
||||||
import { MemoryDetailModal } from "./memory-detail-modal";
|
import { MemoryDetailModal } from "./memory-detail-modal";
|
||||||
import { client } from "@/lib/api";
|
import { client } from "@/lib/api";
|
||||||
|
|
@ -29,6 +29,7 @@ export function MemoryDetailPanel({
|
||||||
const [fullMemory, setFullMemory] = useState<any>(null);
|
const [fullMemory, setFullMemory] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [sourceMemoryModalId, setSourceMemoryModalId] = useState<string | null>(null);
|
const [sourceMemoryModalId, setSourceMemoryModalId] = useState<string | null>(null);
|
||||||
|
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
||||||
|
|
||||||
// Fetch full memory data when panel opens
|
// Fetch full memory data when panel opens
|
||||||
// For mental models, use getMentalModel to get source memories
|
// For mental models, use getMentalModel to get source memories
|
||||||
|
|
@ -296,6 +297,20 @@ export function MemoryDetailPanel({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* View History button (observations only) */}
|
||||||
|
{isObservation && (
|
||||||
|
<div className="border-t border-border pt-5">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full flex items-center gap-2"
|
||||||
|
onClick={() => setHistoryModalOpen(true)}
|
||||||
|
>
|
||||||
|
<History className="h-4 w-4" />
|
||||||
|
View History
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Memory ID */}
|
{/* Memory ID */}
|
||||||
{memoryId && (
|
{memoryId && (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -333,6 +348,15 @@ export function MemoryDetailPanel({
|
||||||
memoryId={sourceMemoryModalId}
|
memoryId={sourceMemoryModalId}
|
||||||
onClose={() => setSourceMemoryModalId(null)}
|
onClose={() => setSourceMemoryModalId(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* History Modal */}
|
||||||
|
{historyModalOpen && memoryId && bankId && (
|
||||||
|
<MemoryDetailModal
|
||||||
|
memoryId={memoryId}
|
||||||
|
onClose={() => setHistoryModalOpen(false)}
|
||||||
|
initialTab="history"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export interface HistoryEntry {
|
||||||
|
previous_text: string;
|
||||||
|
previous_tags: string[];
|
||||||
|
previous_occurred_start: string | null;
|
||||||
|
previous_occurred_end: string | null;
|
||||||
|
previous_mentioned_at: string | null;
|
||||||
|
changed_at: string;
|
||||||
|
new_source_memory_ids: string[];
|
||||||
|
source_facts?: {
|
||||||
|
id: string;
|
||||||
|
text: string | null;
|
||||||
|
type: string | null;
|
||||||
|
context: string | null;
|
||||||
|
is_new: boolean;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CurrentState {
|
||||||
|
text: string;
|
||||||
|
tags: string[];
|
||||||
|
occurred_start: string | null;
|
||||||
|
occurred_end: string | null;
|
||||||
|
mentioned_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function diffWords(a: string, b: string): { type: "same" | "removed" | "added"; text: string }[] {
|
||||||
|
const aWords = a.split(/(\s+)/);
|
||||||
|
const bWords = b.split(/(\s+)/);
|
||||||
|
const m = aWords.length;
|
||||||
|
const n = bWords.length;
|
||||||
|
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
||||||
|
for (let i = 1; i <= m; i++) {
|
||||||
|
for (let j = 1; j <= n; j++) {
|
||||||
|
dp[i][j] =
|
||||||
|
aWords[i - 1] === bWords[j - 1]
|
||||||
|
? dp[i - 1][j - 1] + 1
|
||||||
|
: Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let i = m,
|
||||||
|
j = n;
|
||||||
|
const ops: { type: "same" | "removed" | "added"; text: string }[] = [];
|
||||||
|
while (i > 0 || j > 0) {
|
||||||
|
if (i > 0 && j > 0 && aWords[i - 1] === bWords[j - 1]) {
|
||||||
|
ops.push({ type: "same", text: aWords[i - 1] });
|
||||||
|
i--;
|
||||||
|
j--;
|
||||||
|
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
||||||
|
ops.push({ type: "added", text: bWords[j - 1] });
|
||||||
|
j--;
|
||||||
|
} else {
|
||||||
|
ops.push({ type: "removed", text: aWords[i - 1] });
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ops.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextDiff({ before, after }: { before: string; after: string }) {
|
||||||
|
const parts = diffWords(before, after);
|
||||||
|
const hasChanges = parts.some((p) => p.type !== "same");
|
||||||
|
if (!hasChanges) return <span className="text-sm text-muted-foreground italic">unchanged</span>;
|
||||||
|
return (
|
||||||
|
<span className="text-sm leading-relaxed">
|
||||||
|
{parts.map((part, idx) =>
|
||||||
|
part.type === "same" ? (
|
||||||
|
<span key={idx}>{part.text}</span>
|
||||||
|
) : part.type === "removed" ? (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="bg-red-500/15 text-red-700 dark:text-red-400 line-through rounded-sm px-0.5"
|
||||||
|
>
|
||||||
|
{part.text}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="bg-green-500/15 text-green-700 dark:text-green-400 rounded-sm px-0.5"
|
||||||
|
>
|
||||||
|
{part.text}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagsDiff({ before, after }: { before: string[]; after: string[] }) {
|
||||||
|
const removed = before.filter((t) => !after.includes(t));
|
||||||
|
const added = after.filter((t) => !before.includes(t));
|
||||||
|
const kept = before.filter((t) => after.includes(t));
|
||||||
|
if (removed.length === 0 && added.length === 0)
|
||||||
|
return <span className="text-sm text-muted-foreground italic">unchanged</span>;
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 flex-wrap">
|
||||||
|
{kept.map((t, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="text-[10px] px-1.5 py-0.5 rounded-md bg-amber-500/10 text-amber-700 border border-amber-500/20 font-mono"
|
||||||
|
>
|
||||||
|
#{t}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{removed.map((t, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="text-[10px] px-1.5 py-0.5 rounded-md bg-red-500/15 text-red-700 dark:text-red-400 border border-red-500/20 font-mono line-through"
|
||||||
|
>
|
||||||
|
#{t}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{added.map((t, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="text-[10px] px-1.5 py-0.5 rounded-md bg-green-500/15 text-green-700 dark:text-green-400 border border-green-500/20 font-mono"
|
||||||
|
>
|
||||||
|
+#{t}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DateDiff({
|
||||||
|
label,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
before: string | null;
|
||||||
|
after: string | null;
|
||||||
|
}) {
|
||||||
|
if (!before && !after) return null;
|
||||||
|
const changed = before !== after;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-muted-foreground">{label}: </span>
|
||||||
|
{changed ? (
|
||||||
|
<>
|
||||||
|
<span className="text-xs bg-red-500/15 text-red-700 dark:text-red-400 line-through rounded-sm px-0.5">
|
||||||
|
{before ? new Date(before).toLocaleString() : "—"}
|
||||||
|
</span>
|
||||||
|
{" → "}
|
||||||
|
<span className="text-xs bg-green-500/15 text-green-700 dark:text-green-400 rounded-sm px-0.5">
|
||||||
|
{after ? new Date(after).toLocaleString() : "—"}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs">{after ? new Date(after).toLocaleString() : "—"}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SourceFactItem({ fact }: { fact: NonNullable<HistoryEntry["source_facts"]>[number] }) {
|
||||||
|
const typeColors =
|
||||||
|
fact.type === "experience"
|
||||||
|
? "bg-green-500/10 text-green-700 dark:text-green-400"
|
||||||
|
: "bg-blue-500/10 text-blue-700 dark:text-blue-400";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`p-2 rounded border space-y-1 ${
|
||||||
|
fact.is_new ? "border-green-500/40 bg-green-500/5" : "border-border/50 bg-muted/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{fact.type && (
|
||||||
|
<span
|
||||||
|
className={`text-[10px] px-1.5 py-0.5 rounded font-medium flex-shrink-0 ${typeColors}`}
|
||||||
|
>
|
||||||
|
{fact.type}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{fact.is_new && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded font-medium bg-green-500/15 text-green-700 dark:text-green-400 border border-green-500/30">
|
||||||
|
new
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{fact.context && (
|
||||||
|
<span className="text-[10px] text-muted-foreground italic truncate">{fact.context}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{fact.text ? (
|
||||||
|
<p className="text-xs text-foreground leading-relaxed">{fact.text}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground italic">(memory no longer available)</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ObservationHistoryView({
|
||||||
|
history,
|
||||||
|
current,
|
||||||
|
}: {
|
||||||
|
history: HistoryEntry[];
|
||||||
|
current: CurrentState;
|
||||||
|
}) {
|
||||||
|
// index 0 = most recent change
|
||||||
|
const entries = [...history].reverse();
|
||||||
|
const [idx, setIdx] = useState(0);
|
||||||
|
|
||||||
|
const entry = entries[idx];
|
||||||
|
const isLatest = idx === 0;
|
||||||
|
const afterText = isLatest ? current.text : entries[idx - 1].previous_text;
|
||||||
|
const afterTags = isLatest ? current.tags : entries[idx - 1].previous_tags;
|
||||||
|
const afterOccurredStart = isLatest
|
||||||
|
? current.occurred_start
|
||||||
|
: entries[idx - 1].previous_occurred_start;
|
||||||
|
const afterOccurredEnd = isLatest ? current.occurred_end : entries[idx - 1].previous_occurred_end;
|
||||||
|
const afterMentionedAt = isLatest ? current.mentioned_at : entries[idx - 1].previous_mentioned_at;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Navigation header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Change <span className="font-semibold text-foreground">{history.length - idx}</span> of{" "}
|
||||||
|
{history.length} · {new Date(entry.changed_at).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
disabled={idx === entries.length - 1}
|
||||||
|
onClick={() => setIdx(idx + 1)}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
disabled={idx === 0}
|
||||||
|
onClick={() => setIdx(idx - 1)}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Change card */}
|
||||||
|
<div className="border border-border rounded-lg p-3 space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Text</div>
|
||||||
|
<TextDiff before={entry.previous_text} after={afterText} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Tags</div>
|
||||||
|
<TagsDiff before={entry.previous_tags} after={afterTags} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Dates</div>
|
||||||
|
<DateDiff
|
||||||
|
label="Occurred start"
|
||||||
|
before={entry.previous_occurred_start}
|
||||||
|
after={afterOccurredStart}
|
||||||
|
/>
|
||||||
|
<DateDiff
|
||||||
|
label="Occurred end"
|
||||||
|
before={entry.previous_occurred_end}
|
||||||
|
after={afterOccurredEnd}
|
||||||
|
/>
|
||||||
|
<DateDiff
|
||||||
|
label="Mentioned at"
|
||||||
|
before={entry.previous_mentioned_at}
|
||||||
|
after={afterMentionedAt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{entry.source_facts && entry.source_facts.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
|
||||||
|
Source Facts ({entry.source_facts.length})
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{entry.source_facts.map((fact) => (
|
||||||
|
<SourceFactItem key={fact.id} fact={fact} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -413,9 +413,42 @@ export class ControlPlaneClient {
|
||||||
chunk_id: string | null;
|
chunk_id: string | null;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
observation_scopes: string | string[][] | null;
|
observation_scopes: string | string[][] | null;
|
||||||
|
history?: {
|
||||||
|
previous_text: string;
|
||||||
|
previous_tags: string[];
|
||||||
|
previous_occurred_start: string | null;
|
||||||
|
previous_occurred_end: string | null;
|
||||||
|
previous_mentioned_at: string | null;
|
||||||
|
changed_at: string;
|
||||||
|
new_source_memory_ids: string[];
|
||||||
|
}[];
|
||||||
}>(`/api/memories/${memoryId}?bank_id=${bankId}`);
|
}>(`/api/memories/${memoryId}?bank_id=${bankId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the history of an observation with resolved source facts
|
||||||
|
*/
|
||||||
|
async getObservationHistory(memoryId: string, bankId: string) {
|
||||||
|
return this.fetchApi<
|
||||||
|
{
|
||||||
|
previous_text: string;
|
||||||
|
previous_tags: string[];
|
||||||
|
previous_occurred_start: string | null;
|
||||||
|
previous_occurred_end: string | null;
|
||||||
|
previous_mentioned_at: string | null;
|
||||||
|
changed_at: string;
|
||||||
|
new_source_memory_ids: string[];
|
||||||
|
source_facts: {
|
||||||
|
id: string;
|
||||||
|
text: string | null;
|
||||||
|
type: string | null;
|
||||||
|
context: string | null;
|
||||||
|
is_new: boolean;
|
||||||
|
}[];
|
||||||
|
}[]
|
||||||
|
>(`/api/memories/${memoryId}/history?bank_id=${bankId}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get bank profile
|
* Get bank profile
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,23 @@
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"types/**/*.d.ts"
|
"types/**/*.d.ts",
|
||||||
|
".next-*/types/**/*.ts",
|
||||||
|
".next-*/dev/types/**/*.ts",
|
||||||
|
".next-49944/types/**/*.ts",
|
||||||
|
".next-49944/dev/types/**/*.ts",
|
||||||
|
".next-50612/types/**/*.ts",
|
||||||
|
".next-50612/dev/types/**/*.ts",
|
||||||
|
".next-54508/types/**/*.ts",
|
||||||
|
".next-54508/dev/types/**/*.ts",
|
||||||
|
".next-55630/types/**/*.ts",
|
||||||
|
".next-55630/dev/types/**/*.ts",
|
||||||
|
".next-58976/types/**/*.ts",
|
||||||
|
".next-58976/dev/types/**/*.ts",
|
||||||
|
".next-64080/types/**/*.ts",
|
||||||
|
".next-64080/dev/types/**/*.ts",
|
||||||
|
".next-50432/types/**/*.ts",
|
||||||
|
".next-50432/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,7 @@
|
||||||
"Memory"
|
"Memory"
|
||||||
],
|
],
|
||||||
"summary": "Get memory unit",
|
"summary": "Get memory unit",
|
||||||
"description": "Get a single memory unit by ID with all its metadata including entities and tags.",
|
"description": "Get a single memory unit by ID with all its metadata including entities and tags. Note: the 'history' field is deprecated and always returns an empty list - use GET /memories/{memory_id}/history instead.",
|
||||||
"operationId": "get_memory",
|
"operationId": "get_memory",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -382,6 +382,72 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/default/banks/{bank_id}/memories/{memory_id}/history": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Memory"
|
||||||
|
],
|
||||||
|
"summary": "Get observation history",
|
||||||
|
"description": "Get the full history of an observation, with each change's source facts resolved to their text.",
|
||||||
|
"operationId": "get_observation_history",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "bank_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Bank Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memory_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Memory Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "authorization",
|
||||||
|
"in": "header",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Authorization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/default/banks/{bank_id}/memories/recall": {
|
"/v1/default/banks/{bank_id}/memories/recall": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ echo "✅ SDK built successfully"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo "🚀 Starting Control Plane (Next.js dev server)..."
|
echo "🚀 Starting Control Plane (Next.js dev server)..."
|
||||||
|
# Save caller-provided values before .env can overwrite them
|
||||||
|
_CALLER_PORT="${PORT:-}"
|
||||||
|
_CALLER_DATAPLANE_URL="${HINDSIGHT_CP_DATAPLANE_API_URL:-}"
|
||||||
|
|
||||||
if [ -f "$ROOT_DIR/.env" ]; then
|
if [ -f "$ROOT_DIR/.env" ]; then
|
||||||
echo "📄 Loading environment from $ROOT_DIR/.env"
|
echo "📄 Loading environment from $ROOT_DIR/.env"
|
||||||
# Load env vars from root .env file
|
# Load env vars from root .env file
|
||||||
|
|
@ -28,7 +32,9 @@ fi
|
||||||
|
|
||||||
# Map prefixed env vars to Next.js standard vars
|
# Map prefixed env vars to Next.js standard vars
|
||||||
export HOSTNAME="${HINDSIGHT_CP_HOSTNAME:-0.0.0.0}"
|
export HOSTNAME="${HINDSIGHT_CP_HOSTNAME:-0.0.0.0}"
|
||||||
export PORT="${HINDSIGHT_CP_PORT:-9999}"
|
# Caller-provided values take priority over .env
|
||||||
|
export PORT="${_CALLER_PORT:-${HINDSIGHT_CP_PORT:-9999}}"
|
||||||
|
export HINDSIGHT_CP_DATAPLANE_API_URL="${_CALLER_DATAPLANE_URL:-${HINDSIGHT_CP_DATAPLANE_API_URL:-http://localhost:8888}}"
|
||||||
|
|
||||||
# Run dev server
|
# Run dev server
|
||||||
npm run dev -w @vectorize-io/hindsight-control-plane
|
npm run dev -w @vectorize-io/hindsight-control-plane
|
||||||
|
|
@ -3,6 +3,14 @@ set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# Parse --random-port flag
|
||||||
|
RANDOM_PORT=false
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$arg" = "--random-port" ]; then
|
||||||
|
RANDOM_PORT=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# Load .env to pick up HINDSIGHT_API_PORT if set
|
# Load .env to pick up HINDSIGHT_API_PORT if set
|
||||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||||
if [ -f "$ROOT_DIR/.env" ]; then
|
if [ -f "$ROOT_DIR/.env" ]; then
|
||||||
|
|
@ -10,8 +18,19 @@ if [ -f "$ROOT_DIR/.env" ]; then
|
||||||
source "$ROOT_DIR/.env"
|
source "$ROOT_DIR/.env"
|
||||||
set +a
|
set +a
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
get_free_port() {
|
||||||
|
python3 -c "import socket; s=socket.socket(); s.bind(('', 0)); print(s.getsockname()[1]); s.close()"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$RANDOM_PORT" = true ]; then
|
||||||
|
API_PORT="$(get_free_port)"
|
||||||
|
CP_PORT="$(get_free_port)"
|
||||||
|
echo "Using random ports — API: $API_PORT, Control Plane: $CP_PORT"
|
||||||
|
else
|
||||||
API_PORT="${HINDSIGHT_API_PORT:-8888}"
|
API_PORT="${HINDSIGHT_API_PORT:-8888}"
|
||||||
CP_PORT="${HINDSIGHT_CP_PORT:-9999}"
|
CP_PORT="${HINDSIGHT_CP_PORT:-9999}"
|
||||||
|
fi
|
||||||
|
|
||||||
PIDS=()
|
PIDS=()
|
||||||
|
|
||||||
|
|
@ -37,7 +56,7 @@ trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
# Start API
|
# Start API
|
||||||
echo "Starting API server..."
|
echo "Starting API server..."
|
||||||
"$SCRIPT_DIR/start-api.sh" &
|
"$SCRIPT_DIR/start-api.sh" --port "$API_PORT" &
|
||||||
API_PID=$!
|
API_PID=$!
|
||||||
PIDS+=($API_PID)
|
PIDS+=($API_PID)
|
||||||
|
|
||||||
|
|
@ -63,7 +82,7 @@ fi
|
||||||
|
|
||||||
# Start Control Plane
|
# Start Control Plane
|
||||||
echo ""
|
echo ""
|
||||||
"$SCRIPT_DIR/start-control-plane.sh" &
|
PORT="$CP_PORT" HINDSIGHT_CP_DATAPLANE_API_URL="http://localhost:${API_PORT}" "$SCRIPT_DIR/start-control-plane.sh" &
|
||||||
CP_PID=$!
|
CP_PID=$!
|
||||||
PIDS+=($CP_PID)
|
PIDS+=($CP_PID)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue