Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0022d427d3 | ||
|
|
1f9bad0858 | ||
|
|
138bf02f29 | ||
|
|
df178aae8a | ||
|
|
a43026b8f4 | ||
|
|
5c425e276e | ||
|
|
265192e509 | ||
|
|
8f2cee4568 | ||
|
|
92f433c904 | ||
|
|
f8ce15b9bf | ||
|
|
d68f618969 | ||
|
|
33e9db64a1 | ||
|
|
0c8699dc20 | ||
|
|
251c451fc3 | ||
|
|
8ed49e4387 | ||
|
|
82afa76182 | ||
|
|
4c307ce4e7 | ||
|
|
252b013243 | ||
|
|
a6b0f82124 | ||
|
|
a27754fb15 | ||
|
|
40fe7aac86 | ||
|
|
7393400f34 | ||
|
|
3b7d18d474 | ||
|
|
6e18858e32 | ||
|
|
84e67efbf4 | ||
|
|
ef2e8ab7ff | ||
|
|
cc45e16904 | ||
|
|
82b01ace5e | ||
|
|
962140eef6 | ||
|
|
5e73d5ff62 | ||
|
|
2c47b8b0d5 | ||
|
|
00968a1ce4 | ||
|
|
b7080a16cf | ||
|
|
c0aed313f4 | ||
|
|
2c53629420 | ||
|
|
760bfc7447 | ||
|
|
cce0a2cb39 | ||
|
|
1c1cf4ce56 | ||
|
|
a99a1ebf9b | ||
|
|
12a6739fc9 | ||
|
|
d8ee10a78d | ||
|
|
ab01144b26 | ||
|
|
072b3278ba | ||
|
|
74e82a3ea8 | ||
|
|
a5b752a983 | ||
|
|
7b878f89a7 | ||
|
|
1b92c8230f | ||
|
|
0178d91333 | ||
|
|
017b8d7271 | ||
|
|
21176f8ee8 | ||
|
|
74bdfc9475 |
@@ -10,6 +10,17 @@ HINDSIGHT_API_LLM_BASE_URL=https://api.openai.com/v1
|
||||
# Reasoning effort for providers/models that support it. Examples: low, medium, high, xhigh.
|
||||
# HINDSIGHT_API_LLM_REASONING_EFFORT=low
|
||||
|
||||
# Sampling temperature for internal LLM calls. Set a number in [0.0, 2.0], or `none`
|
||||
# to omit the temperature parameter entirely (required for models that reject explicit
|
||||
# temperatures, e.g. Azure gpt-5.5). The global override below applies to every operation;
|
||||
# per-operation overrides (defaults: verification=0.0, retain=0.1, reflect=0.9,
|
||||
# consolidation=0.0) take precedence.
|
||||
# HINDSIGHT_API_LLM_TEMPERATURE=none
|
||||
# HINDSIGHT_API_LLM_TEMPERATURE_VERIFICATION=0.0
|
||||
# HINDSIGHT_API_LLM_TEMPERATURE_RETAIN=0.1
|
||||
# HINDSIGHT_API_LLM_TEMPERATURE_REFLECT=0.9
|
||||
# HINDSIGHT_API_LLM_TEMPERATURE_CONSOLIDATION=0.0
|
||||
|
||||
# Example: Anthropic Claude configuration
|
||||
# HINDSIGHT_API_LLM_PROVIDER=anthropic
|
||||
# HINDSIGHT_API_LLM_API_KEY=your-anthropic-api-key
|
||||
|
||||
@@ -6,6 +6,7 @@ dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
.mcp.json
|
||||
.playwright-mcp/
|
||||
.osgrep
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
[ 2810ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 2864ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 3193ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 3194ms] [LOG] [Fast Refresh] done in 244ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 3195ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 3296ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 3749ms] [LOG] [Fast Refresh] done in 553ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 3847ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/version:0
|
||||
[ 3848ms] [ERROR] Error loading features: Error: Failed to get version
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async FeaturesProvider.useEffect.loadFeatures (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:1039:42) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 3894ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/banks:0
|
||||
[ 3894ms] [ERROR] Error loading banks: Error: Failed to fetch banks
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async loadBanks (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:924:30) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 4149ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/stats/locomo-demo-v3/memories-timeseries?period=7d&time_field=created_at:0
|
||||
[ 4149ms] [ERROR] Error loading memories timeseries: Error: Failed to fetch memories timeseries
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async loadTimeseries (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-stats-view_tsx_09_9kz8._.js:1530:26) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 4204ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/operations/locomo-demo-v3?limit=10&exclude_parents=true:0
|
||||
[ 4204ms] [ERROR] Error loading operations: Error: Failed to fetch operations
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async BankOperationsView.useCallback[loadOperations] (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-operations-view_tsx_1xto18i._.js:378:33) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 4251ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/stats/locomo-demo-v3:0
|
||||
[ 4251ms] [ERROR] Error loading bank stats: Error: Failed to fetch stats
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async Promise.all (index 0)
|
||||
at async loadData (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-stats-view_tsx_09_9kz8._.js:1515:51) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 4305ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/locomo-demo-v3/mental-models:0
|
||||
[ 4350ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/version:0
|
||||
[ 4411ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/stats/locomo-demo-v3:0
|
||||
[ 4471ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/profile/locomo-demo-v3:0
|
||||
[ 4526ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/locomo-demo-v3/directives:0
|
||||
[ 8276ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/stats/locomo-demo-v3:0
|
||||
[ 8277ms] [ERROR] Error loading bank stats: Error: Failed to fetch stats
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async Promise.all (index 0)
|
||||
at async loadData (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-stats-view_tsx_09_9kz8._.js:1515:51) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 8298ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 8413ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/locomo-demo-v3/mental-models:0
|
||||
[ 8419ms] [LOG] [Fast Refresh] done in 223ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 8453ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/stats/locomo-demo-v3/memories-timeseries?period=7d&time_field=created_at:0
|
||||
[ 8454ms] [ERROR] Error loading memories timeseries: Error: Failed to fetch memories timeseries
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async loadTimeseries (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-stats-view_tsx_09_9kz8._.js:1530:26) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 8570ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 8627ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/operations/locomo-demo-v3?limit=10&exclude_parents=true:0
|
||||
[ 8628ms] [ERROR] Error loading operations: Error: Failed to fetch operations
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async BankOperationsView.useCallback[loadOperations] (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-operations-view_tsx_1xto18i._.js:378:33) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 8681ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/locomo-demo-v3/directives:0
|
||||
[ 8681ms] [ERROR] Error refreshing stats: Error: Failed to list directives
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async Promise.all (index 1)
|
||||
at async loadData (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-profile-view_tsx_046c-92._.js:126:53) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 8687ms] [LOG] [Fast Refresh] done in 217ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 8797ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 8891ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/stats/locomo-demo-v3:0
|
||||
[ 8898ms] [LOG] [Fast Refresh] done in 201ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 13292ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 13306ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/stats/locomo-demo-v3:0
|
||||
[ 13307ms] [ERROR] Error loading bank stats: Error: Failed to fetch stats
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async Promise.all (index 0)
|
||||
at async loadData (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-stats-view_tsx_09_9kz8._.js:1515:51) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 13409ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/locomo-demo-v3/mental-models:0
|
||||
[ 13413ms] [LOG] [Fast Refresh] done in 221ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 13515ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/stats/locomo-demo-v3/memories-timeseries?period=7d&time_field=created_at:0
|
||||
[ 13515ms] [ERROR] Error loading memories timeseries: Error: Failed to fetch memories timeseries
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async loadTimeseries (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-stats-view_tsx_09_9kz8._.js:1530:26) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 13557ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/operations/locomo-demo-v3?limit=10&exclude_parents=true:0
|
||||
[ 13558ms] [ERROR] Error loading operations: Error: Failed to fetch operations
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async BankOperationsView.useCallback[loadOperations] (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-operations-view_tsx_1xto18i._.js:378:33) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 13622ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/locomo-demo-v3/directives:0
|
||||
[ 13624ms] [ERROR] Error refreshing stats: Error: Failed to list directives
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async Promise.all (index 1)
|
||||
at async loadData (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-profile-view_tsx_046c-92._.js:126:53) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 13664ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/stats/locomo-demo-v3:0
|
||||
[ 18234ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/stats/locomo-demo-v3:0
|
||||
[ 18235ms] [ERROR] Error loading bank stats: Error: Failed to fetch stats
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async Promise.all (index 0)
|
||||
at async loadData (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-stats-view_tsx_09_9kz8._.js:1515:51) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 18270ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/locomo-demo-v3/mental-models:0
|
||||
[ 18313ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/stats/locomo-demo-v3/memories-timeseries?period=7d&time_field=created_at:0
|
||||
[ 18313ms] [ERROR] Error loading memories timeseries: Error: Failed to fetch memories timeseries
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async loadTimeseries (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-stats-view_tsx_09_9kz8._.js:1530:26) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 18360ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/operations/locomo-demo-v3?limit=10&exclude_parents=true:0
|
||||
[ 18360ms] [ERROR] Error loading operations: Error: Failed to fetch operations
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async BankOperationsView.useCallback[loadOperations] (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-operations-view_tsx_1xto18i._.js:378:33) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 18393ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/locomo-demo-v3/directives:0
|
||||
[ 18394ms] [ERROR] Error refreshing stats: Error: Failed to list directives
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async Promise.all (index 1)
|
||||
at async loadData (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-profile-view_tsx_046c-92._.js:126:53) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 18436ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/stats/locomo-demo-v3:0
|
||||
@@ -1,13 +0,0 @@
|
||||
[ 97ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 110ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 256ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 287ms] [ERROR] Failed to load resource: the server responded with a status of 502 (Bad Gateway) @ http://localhost:9999/api/banks:0
|
||||
[ 287ms] [ERROR] Error loading banks: Error: Failed to fetch banks
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async loadBanks (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:924:30) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 370ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/version:0
|
||||
[ 371ms] [ERROR] Error loading features: Error: Failed to get version
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async FeaturesProvider.useEffect.loadFeatures (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:1039:42) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 450ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/version:0
|
||||
[ 518ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/graph?bank_id=locomo-demo-v3&type=world&limit=1000:0
|
||||
@@ -1,3 +0,0 @@
|
||||
[ 128ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 143ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 290ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
@@ -1,7 +0,0 @@
|
||||
[ 93ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 103ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 242ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 160582ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 160633ms] [LOG] [Fast Refresh] done in 90ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 167220ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 167249ms] [LOG] [Fast Refresh] done in 70ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
@@ -1,317 +0,0 @@
|
||||
[ 177ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 187ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 356ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 197479ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 198209ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:585:87)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4620:24)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198213ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:586:80)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4620:24)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198213ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:585:87)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4620:24)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198213ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:586:80)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4620:24)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198213ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:585:87)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4620:24)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198213ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:586:80)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4620:24)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198215ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:585:87)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooksAgain (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4675:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4626:28)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198215ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:586:80)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooksAgain (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4675:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4626:28)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198215ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:585:87)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooksAgain (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4675:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4626:28)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198215ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:586:80)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooksAgain (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4675:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4626:28)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198215ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:585:87)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooksAgain (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4675:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4626:28)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198216ms] [ERROR] IntlError: MISSING_MESSAGE: Could not resolve `nav.bank.copyName` in messages for locale `en`.
|
||||
at getFallbackFromErrorAndNotify (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3595:23)
|
||||
at translateBaseFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3625:28)
|
||||
at translateFn (http://localhost:9999/_next/static/chunks/node_modules_1ytpq7s._.js:3653:24)
|
||||
at eval (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:586:80)
|
||||
at Array.map (<anonymous>)
|
||||
at BankSelectorInner (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_components_bank-selector_tsx_1264hts._.js?id=%255Bproject%255D%252Fhindsight-control-plane%252Fsrc%252Fcomponents%252Fbank-selector.tsx+%255Bapp-client%255D+%2528ecmascript%2529:539:71)
|
||||
at Object.react_stack_bottom_frame (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:15037:24)
|
||||
at renderWithHooksAgain (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4675:24)
|
||||
at renderWithHooks (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:4626:28)
|
||||
at updateFunctionComponent (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6081:21)
|
||||
at beginWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:6691:24)
|
||||
at runWithFiberInDEV (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:965:74)
|
||||
at performUnitOfWork (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9555:97)
|
||||
at workLoopSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9449:40)
|
||||
at renderRootSync (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9433:13)
|
||||
at performWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9061:186)
|
||||
at performSyncWorkOnRoot (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10263:9)
|
||||
at flushSyncWorkAcrossRoots_impl (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:10179:316)
|
||||
at flushSyncWork$1 (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:9230:86)
|
||||
at Object.scheduleRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_react-dom_096_9a-._.js:299:13)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:391:33
|
||||
at Set.forEach (<anonymous>)
|
||||
at Object.performReactRefresh (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:384:38)
|
||||
at applyUpdate (http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:878:31)
|
||||
at http://localhost:9999/_next/static/chunks/node_modules_next_dist_compiled_1amofcm._.js:886:13 @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[ 198220ms] [LOG] [Fast Refresh] done in 824ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 198368ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 198424ms] [LOG] [Fast Refresh] done in 158ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 198686ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 198784ms] [LOG] [Fast Refresh] done in 199ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 249494ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 249512ms] [LOG] [Fast Refresh] done in 75ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
@@ -1,256 +0,0 @@
|
||||
[ 915ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 923ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 1064ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 41527ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[ 41548ms] [LOG] [Fast Refresh] done in 75ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[245534553ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[245534675ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[245534692ms] [LOG] [Fast Refresh] done in 118ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[250969674ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[254160733ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[254160869ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[254160894ms] [LOG] [Fast Refresh] done in 126ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[258259347ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[258260528ms] [LOG] [Fast Refresh] done in 1288ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[258723297ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[258775713ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[261264735ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[261269535ms] [LOG] [Fast Refresh] done in 4906ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[261546917ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[261547880ms] [LOG] [Fast Refresh] done in 1134ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[261663747ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[261664233ms] [LOG] [Fast Refresh] done in 587ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[342782138ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[342783538ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[342784058ms] [LOG] [Fast Refresh] done in 1772ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[342784780ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[342785284ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[342785307ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[342785496ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[343047107ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[343048273ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[343048852ms] [LOG] [Fast Refresh] done in 1255ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[343048891ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[343049198ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[343049742ms] [LOG] [Fast Refresh] done in 645ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[343049828ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[343049841ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[343049993ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345632147ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345633682ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345633938ms] [LOG] [Fast Refresh] done in 1702ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345633963ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345634647ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345634678ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345634837ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345684130ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345685275ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345685434ms] [LOG] [Fast Refresh] done in 1243ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345685484ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345685649ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345685690ms] [LOG] [Fast Refresh] done in 142ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345686169ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345686179ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345686314ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345686333ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[345686379ms] [LOG] [Fast Refresh] done in 146ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[357996647ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[357998178ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[357998523ms] [LOG] [Fast Refresh] done in 1726ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[357998605ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[357999819ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[357999851ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358000036ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358003608ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks:0
|
||||
[358003608ms] [ERROR] Error loading banks: Error: HTTP 500
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:228:31)
|
||||
at async loadBanks (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:933:30) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[358003695ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/graph?bank_id=cluster-demo&type=observation&limit=1000:0
|
||||
[358003736ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/cluster-demo/observations/scopes:0
|
||||
[358007258ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/cluster-demo/observations/scopes:0
|
||||
[358308031ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358309424ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358309434ms] [LOG] [Fast Refresh] done in 1494ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358309664ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358310163ms] [LOG] [Fast Refresh] done in 601ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358310217ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358310226ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358310366ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358310395ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358310402ms] [LOG] [Fast Refresh] done in 109ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[358313929ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks:0
|
||||
[358313930ms] [ERROR] Error loading banks: Error: Failed to fetch banks
|
||||
at ControlPlaneClient.fetchApi (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:219:31)
|
||||
at async loadBanks (http://localhost:9999/_next/static/chunks/hindsight-control-plane_src_09ldbjk._.js:924:30) @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:3318
|
||||
[358314000ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/graph?bank_id=cluster-demo&type=observation&limit=1000:0
|
||||
[358314029ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/cluster-demo/observations/scopes:0
|
||||
[358317549ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:9999/api/banks/cluster-demo/observations/scopes:0
|
||||
[430163774ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430165839ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430165884ms] [LOG] [Fast Refresh] done in 2220ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430165884ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430166287ms] [LOG] [Fast Refresh] done in 117ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430167720ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430167745ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430167941ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430167941ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430168007ms] [LOG] [Fast Refresh] done in 198ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430168269ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430168278ms] [LOG] [Fast Refresh] done in 111ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430168514ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430168519ms] [LOG] [Fast Refresh] done in 106ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430245962ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430247979ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430248692ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430248710ms] [LOG] [Fast Refresh] done in 2832ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430252503ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430252672ms] [LOG] [Fast Refresh] done in 270ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430252866ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430252942ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430253185ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430253287ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[430253424ms] [LOG] [Fast Refresh] done in 238ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431414177ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431417650ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431423427ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431423463ms] [LOG] [Fast Refresh] done in 9359ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431423725ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431423755ms] [LOG] [Fast Refresh] done in 131ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431424487ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431424746ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431425112ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431425235ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431425478ms] [LOG] [Fast Refresh] done in 343ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431426290ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431426379ms] [LOG] [Fast Refresh] done in 190ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431427470ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[431427485ms] [LOG] [Fast Refresh] done in 116ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[432835456ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[432835589ms] [LOG] [Fast Refresh] done in 246ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[432835840ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[432838224ms] [WARNING] [Fast Refresh] performing full reload
|
||||
|
||||
Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.
|
||||
You might have a file which exports a React component but also exports a value that is imported by a non-React component file.
|
||||
Consider migrating the non-React component export to a separate file and importing it into both files.
|
||||
|
||||
It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.
|
||||
Fast Refresh requires at least one parent function component in your React tree. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[432838319ms] [LOG] [Fast Refresh] done in 2480ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[432839972ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[432839994ms] [LOG] [HMR] connected @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[432840167ms] [WARNING] Image with src "http://localhost:9999/logo.png" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[432933812ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
[432934449ms] [LOG] [Fast Refresh] done in 740ms @ http://localhost:9999/_next/static/chunks/node_modules_next_dist_1ybzpk2._.js:2477
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: locomo-demo-v3
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- generic [ref=e64]:
|
||||
- generic [ref=e65]:
|
||||
- heading "Bank Configuration" [level=1] [ref=e66]
|
||||
- paragraph [ref=e67]: Manage bank settings, profile, and operations.
|
||||
- button "Actions" [ref=e68]:
|
||||
- text: Actions
|
||||
- img
|
||||
- generic [ref=e70]:
|
||||
- button "General" [ref=e71]: General
|
||||
- button "Webhooks" [ref=e73]
|
||||
- button "Audit LogsOff" [ref=e74]
|
||||
- button "LLM RequestsOff" [ref=e75]
|
||||
- generic [ref=e77]:
|
||||
- paragraph [ref=e78]: Overview statistics and background operations for this memory bank.
|
||||
- generic [ref=e79]:
|
||||
- img [ref=e81]
|
||||
- generic [ref=e84]:
|
||||
- generic [ref=e85]:
|
||||
- generic [ref=e86]:
|
||||
- generic [ref=e87]:
|
||||
- heading "Background Operations" [level=3] [ref=e88]
|
||||
- button "Refresh operations" [disabled] [ref=e89]:
|
||||
- img [ref=e90]
|
||||
- paragraph [ref=e95]: 0 operations
|
||||
- generic [ref=e96]:
|
||||
- combobox [ref=e97]:
|
||||
- generic:
|
||||
- generic:
|
||||
- generic: All types
|
||||
- img [ref=e98]
|
||||
- generic [ref=e100]:
|
||||
- button "All" [ref=e101]
|
||||
- button "Pending" [ref=e102]
|
||||
- button "Processing" [ref=e103]
|
||||
- button "Completed" [ref=e104]
|
||||
- button "Failed" [ref=e105]
|
||||
- button "Cancelled" [ref=e106]
|
||||
- paragraph [ref=e108]: No operations
|
||||
- generic [ref=e110]:
|
||||
- img [ref=e111]
|
||||
- generic [ref=e114]: Loading profile...
|
||||
- region "Notifications alt+T"
|
||||
- alert [ref=e115]
|
||||
@@ -1,66 +0,0 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: locomo-demo-v3
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/locomo-demo-v3?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]: World Facts
|
||||
- button "Experience" [ref=e70]
|
||||
- button "ObservationsOff" [ref=e71]
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e74]:
|
||||
- paragraph [ref=e75]: Objective facts about the world received from external sources.
|
||||
- generic [ref=e77]:
|
||||
- img [ref=e78]
|
||||
- paragraph [ref=e83]: Loading memories...
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,66 +0,0 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]: World Facts
|
||||
- button "Experience" [ref=e70]
|
||||
- button "ObservationsOff" [ref=e71]
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e74]:
|
||||
- paragraph [ref=e75]: Objective facts about the world received from external sources.
|
||||
- generic [ref=e77]:
|
||||
- img [ref=e78]
|
||||
- paragraph [ref=e83]: Loading memories...
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,113 +0,0 @@
|
||||
- generic [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]
|
||||
- button "Experience" [active] [ref=e70]: Experience
|
||||
- button "Observations" [ref=e94]
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e170]:
|
||||
- paragraph [ref=e171]: The bank's own actions, interactions, and first-person experiences.
|
||||
- generic [ref=e172]:
|
||||
- generic [ref=e174]:
|
||||
- generic [ref=e175]:
|
||||
- img
|
||||
- textbox "Filter by text or context (press Enter)..." [ref=e176]
|
||||
- generic [ref=e178]:
|
||||
- img
|
||||
- textbox "Filter by tag…" [ref=e179]
|
||||
- generic [ref=e180]:
|
||||
- generic [ref=e182]: 1 total memories
|
||||
- generic [ref=e183]:
|
||||
- button "Constellation" [ref=e184]:
|
||||
- img [ref=e185]
|
||||
- text: Constellation
|
||||
- button "Graph" [ref=e192]:
|
||||
- img [ref=e193]
|
||||
- text: Graph
|
||||
- button "Table" [ref=e198]:
|
||||
- img [ref=e199]
|
||||
- text: Table
|
||||
- button "Timeline" [ref=e200]:
|
||||
- img [ref=e201]
|
||||
- text: Timeline
|
||||
- generic [ref=e203]:
|
||||
- generic [ref=e207]:
|
||||
- button "Share" [ref=e208] [cursor=pointer]:
|
||||
- img [ref=e209]
|
||||
- text: Share
|
||||
- button "Fullscreen" [ref=e212] [cursor=pointer]:
|
||||
- img [ref=e213]
|
||||
- text: Fullscreen
|
||||
- button "Hide panel" [ref=e218]:
|
||||
- img [ref=e219]
|
||||
- generic [ref=e222]:
|
||||
- heading "Constellation View" [level=3] [ref=e223]
|
||||
- paragraph [ref=e224]: Canvas-rendered memory map with spatial label deconfliction. Scroll to zoom, drag to pan, hover to explore entity connections. Click a memory to view details.
|
||||
- generic [ref=e225]:
|
||||
- heading "Color by" [level=4] [ref=e226]
|
||||
- combobox [ref=e227]:
|
||||
- generic: Mentioned
|
||||
- img [ref=e228]
|
||||
- generic [ref=e230]:
|
||||
- heading "Link types" [level=4] [ref=e231]
|
||||
- generic [ref=e234] [cursor=pointer]: semantic
|
||||
- generic [ref=e237] [cursor=pointer]: temporal
|
||||
- generic [ref=e240] [cursor=pointer]: entity
|
||||
- generic [ref=e243] [cursor=pointer]: causal
|
||||
- generic [ref=e244]:
|
||||
- generic [ref=e245]: "Nodes: 1"
|
||||
- generic [ref=e246]: "Links: 1"
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,126 +0,0 @@
|
||||
- generic [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]
|
||||
- button "Experience" [ref=e70]
|
||||
- button "Observations" [active] [ref=e94]: Observations
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e248]:
|
||||
- paragraph [ref=e249]: Consolidated knowledge synthesized from facts — patterns, preferences, and learnings that emerge from accumulated evidence.
|
||||
- generic [ref=e250]:
|
||||
- generic [ref=e252]:
|
||||
- generic [ref=e253]:
|
||||
- img
|
||||
- textbox "Filter by text or context (press Enter)..." [ref=e254]
|
||||
- generic [ref=e256]:
|
||||
- img
|
||||
- textbox "Filter by tag…" [ref=e257]
|
||||
- combobox "Scope" [ref=e258]:
|
||||
- img
|
||||
- generic [ref=e260]: All scopes
|
||||
- img
|
||||
- generic [ref=e261]:
|
||||
- generic [ref=e262]:
|
||||
- generic [ref=e263]: 163 total memories
|
||||
- 'generic "All memories consolidated (last: 6/11/2026, 5:58:22 PM)" [ref=e264]':
|
||||
- img [ref=e265]
|
||||
- text: In Sync
|
||||
- generic [ref=e268]:
|
||||
- button "Constellation" [ref=e269]:
|
||||
- img [ref=e270]
|
||||
- text: Constellation
|
||||
- button "Graph" [ref=e277]:
|
||||
- img [ref=e278]
|
||||
- text: Graph
|
||||
- button "Table" [ref=e283]:
|
||||
- img [ref=e284]
|
||||
- text: Table
|
||||
- button "Timeline" [ref=e285]:
|
||||
- img [ref=e286]
|
||||
- text: Timeline
|
||||
- generic [ref=e288]:
|
||||
- generic [ref=e292]:
|
||||
- button "Share" [ref=e293] [cursor=pointer]:
|
||||
- img [ref=e294]
|
||||
- text: Share
|
||||
- button "Fullscreen" [ref=e297] [cursor=pointer]:
|
||||
- img [ref=e298]
|
||||
- text: Fullscreen
|
||||
- button "Hide panel" [ref=e303]:
|
||||
- img [ref=e304]
|
||||
- generic [ref=e307]:
|
||||
- heading "Constellation View" [level=3] [ref=e308]
|
||||
- paragraph [ref=e309]: Canvas-rendered memory map with spatial label deconfliction. Scroll to zoom, drag to pan, hover to explore entity connections. Click a memory to view details.
|
||||
- generic [ref=e310]:
|
||||
- generic [ref=e311]:
|
||||
- img [ref=e312]
|
||||
- heading "Group by scope" [level=4] [ref=e316]
|
||||
- switch [ref=e317] [cursor=pointer]
|
||||
- generic [ref=e318]:
|
||||
- heading "Color by" [level=4] [ref=e319]
|
||||
- combobox [ref=e320]:
|
||||
- generic: Mentioned
|
||||
- img [ref=e321]
|
||||
- generic [ref=e323]:
|
||||
- heading "Link types" [level=4] [ref=e324]
|
||||
- generic [ref=e327] [cursor=pointer]: semantic
|
||||
- generic [ref=e330] [cursor=pointer]: temporal
|
||||
- generic [ref=e333] [cursor=pointer]: entity
|
||||
- generic [ref=e336] [cursor=pointer]: causal
|
||||
- generic [ref=e337]:
|
||||
- generic [ref=e338]: "Nodes: 163"
|
||||
- generic [ref=e339]: "Links: 12563"
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,66 +0,0 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]
|
||||
- button "Experience" [ref=e69]: Experience
|
||||
- button "ObservationsOff" [ref=e71]
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e74]:
|
||||
- paragraph [ref=e75]: The bank's own actions, interactions, and first-person experiences.
|
||||
- generic [ref=e77]:
|
||||
- img [ref=e78]
|
||||
- paragraph [ref=e83]: Loading memories...
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,66 +0,0 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]: World Facts
|
||||
- button "Experience" [ref=e70]
|
||||
- button "Observations" [ref=e71]
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e74]:
|
||||
- paragraph [ref=e75]: Objective facts about the world received from external sources.
|
||||
- generic [ref=e77]:
|
||||
- img [ref=e78]
|
||||
- paragraph [ref=e83]: Loading memories...
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,113 +0,0 @@
|
||||
- generic [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]
|
||||
- button "Experience" [active] [ref=e70]: Experience
|
||||
- button "Observations" [ref=e71]
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e95]:
|
||||
- paragraph [ref=e96]: The bank's own actions, interactions, and first-person experiences.
|
||||
- generic [ref=e97]:
|
||||
- generic [ref=e99]:
|
||||
- generic [ref=e100]:
|
||||
- img
|
||||
- textbox "Filter by text or context (press Enter)..." [ref=e101]
|
||||
- generic [ref=e103]:
|
||||
- img
|
||||
- textbox "Filter by tag…" [ref=e104]
|
||||
- generic [ref=e105]:
|
||||
- generic [ref=e107]: 1 total memories
|
||||
- generic [ref=e108]:
|
||||
- button "Constellation" [ref=e109]:
|
||||
- img [ref=e110]
|
||||
- text: Constellation
|
||||
- button "Graph" [ref=e117]:
|
||||
- img [ref=e118]
|
||||
- text: Graph
|
||||
- button "Table" [ref=e123]:
|
||||
- img [ref=e124]
|
||||
- text: Table
|
||||
- button "Timeline" [ref=e125]:
|
||||
- img [ref=e126]
|
||||
- text: Timeline
|
||||
- generic [ref=e128]:
|
||||
- generic [ref=e132]:
|
||||
- button "Share" [ref=e133] [cursor=pointer]:
|
||||
- img [ref=e134]
|
||||
- text: Share
|
||||
- button "Fullscreen" [ref=e137] [cursor=pointer]:
|
||||
- img [ref=e138]
|
||||
- text: Fullscreen
|
||||
- button "Hide panel" [ref=e143]:
|
||||
- img [ref=e144]
|
||||
- generic [ref=e147]:
|
||||
- heading "Constellation View" [level=3] [ref=e148]
|
||||
- paragraph [ref=e149]: Canvas-rendered memory map with spatial label deconfliction. Scroll to zoom, drag to pan, hover to explore entity connections. Click a memory to view details.
|
||||
- generic [ref=e150]:
|
||||
- heading "Color by" [level=4] [ref=e151]
|
||||
- combobox [ref=e152]:
|
||||
- generic: Mentioned
|
||||
- img [ref=e153]
|
||||
- generic [ref=e155]:
|
||||
- heading "Link types" [level=4] [ref=e156]
|
||||
- generic [ref=e159] [cursor=pointer]: semantic
|
||||
- generic [ref=e162] [cursor=pointer]: temporal
|
||||
- generic [ref=e165] [cursor=pointer]: entity
|
||||
- generic [ref=e168] [cursor=pointer]: causal
|
||||
- generic [ref=e169]:
|
||||
- generic [ref=e170]: "Nodes: 1"
|
||||
- generic [ref=e171]: "Links: 1"
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,113 +0,0 @@
|
||||
- generic [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]
|
||||
- button "Experience" [active] [ref=e70]: Experience
|
||||
- button "Observations" [ref=e71]
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e95]:
|
||||
- paragraph [ref=e96]: The bank's own actions, interactions, and first-person experiences.
|
||||
- generic [ref=e97]:
|
||||
- generic [ref=e99]:
|
||||
- generic [ref=e100]:
|
||||
- img
|
||||
- textbox "Filter by text or context (press Enter)..." [ref=e101]
|
||||
- generic [ref=e103]:
|
||||
- img
|
||||
- textbox "Filter by tag…" [ref=e104]
|
||||
- generic [ref=e105]:
|
||||
- generic [ref=e107]: 1 total memories
|
||||
- generic [ref=e108]:
|
||||
- button "Constellation" [ref=e109]:
|
||||
- img [ref=e110]
|
||||
- text: Constellation
|
||||
- button "Graph" [ref=e117]:
|
||||
- img [ref=e118]
|
||||
- text: Graph
|
||||
- button "Table" [ref=e123]:
|
||||
- img [ref=e124]
|
||||
- text: Table
|
||||
- button "Timeline" [ref=e125]:
|
||||
- img [ref=e126]
|
||||
- text: Timeline
|
||||
- generic [ref=e128]:
|
||||
- generic [ref=e132]:
|
||||
- button "Share" [ref=e133] [cursor=pointer]:
|
||||
- img [ref=e134]
|
||||
- text: Share
|
||||
- button "Fullscreen" [ref=e137] [cursor=pointer]:
|
||||
- img [ref=e138]
|
||||
- text: Fullscreen
|
||||
- button "Hide panel" [ref=e143]:
|
||||
- img [ref=e144]
|
||||
- generic [ref=e147]:
|
||||
- heading "Constellation View" [level=3] [ref=e148]
|
||||
- paragraph [ref=e149]: Canvas-rendered memory map with spatial label deconfliction. Scroll to zoom, drag to pan, hover to explore entity connections. Click a memory to view details.
|
||||
- generic [ref=e150]:
|
||||
- heading "Color by" [level=4] [ref=e151]
|
||||
- combobox [ref=e152]:
|
||||
- generic: Mentioned
|
||||
- img [ref=e153]
|
||||
- generic [ref=e155]:
|
||||
- heading "Link types" [level=4] [ref=e156]
|
||||
- generic [ref=e159] [cursor=pointer]: semantic
|
||||
- generic [ref=e162] [cursor=pointer]: temporal
|
||||
- generic [ref=e165] [cursor=pointer]: entity
|
||||
- generic [ref=e168] [cursor=pointer]: causal
|
||||
- generic [ref=e169]:
|
||||
- generic [ref=e170]: "Nodes: 1"
|
||||
- generic [ref=e171]: "Links: 1"
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,126 +0,0 @@
|
||||
- generic [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]
|
||||
- button "Experience" [ref=e70]
|
||||
- button "Observations" [active] [ref=e71]: Observations
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e173]:
|
||||
- paragraph [ref=e174]: Consolidated knowledge synthesized from facts — patterns, preferences, and learnings that emerge from accumulated evidence.
|
||||
- generic [ref=e175]:
|
||||
- generic [ref=e177]:
|
||||
- generic [ref=e178]:
|
||||
- img
|
||||
- textbox "Filter by text or context (press Enter)..." [ref=e179]
|
||||
- generic [ref=e181]:
|
||||
- img
|
||||
- textbox "Filter by tag…" [ref=e182]
|
||||
- combobox "Scope" [ref=e183]:
|
||||
- img
|
||||
- generic [ref=e185]: All scopes
|
||||
- img
|
||||
- generic [ref=e186]:
|
||||
- generic [ref=e187]:
|
||||
- generic [ref=e188]: 163 total memories
|
||||
- 'generic "All memories consolidated (last: 6/11/2026, 5:58:22 PM)" [ref=e189]':
|
||||
- img [ref=e190]
|
||||
- text: In Sync
|
||||
- generic [ref=e193]:
|
||||
- button "Constellation" [ref=e194]:
|
||||
- img [ref=e195]
|
||||
- text: Constellation
|
||||
- button "Graph" [ref=e202]:
|
||||
- img [ref=e203]
|
||||
- text: Graph
|
||||
- button "Table" [ref=e208]:
|
||||
- img [ref=e209]
|
||||
- text: Table
|
||||
- button "Timeline" [ref=e210]:
|
||||
- img [ref=e211]
|
||||
- text: Timeline
|
||||
- generic [ref=e213]:
|
||||
- generic [ref=e217]:
|
||||
- button "Share" [ref=e218] [cursor=pointer]:
|
||||
- img [ref=e219]
|
||||
- text: Share
|
||||
- button "Fullscreen" [ref=e222] [cursor=pointer]:
|
||||
- img [ref=e223]
|
||||
- text: Fullscreen
|
||||
- button "Hide panel" [ref=e228]:
|
||||
- img [ref=e229]
|
||||
- generic [ref=e232]:
|
||||
- heading "Constellation View" [level=3] [ref=e233]
|
||||
- paragraph [ref=e234]: Canvas-rendered memory map with spatial label deconfliction. Scroll to zoom, drag to pan, hover to explore entity connections. Click a memory to view details.
|
||||
- generic [ref=e235]:
|
||||
- generic [ref=e236]:
|
||||
- img [ref=e237]
|
||||
- heading "Group by scope" [level=4] [ref=e241]
|
||||
- switch [ref=e242] [cursor=pointer]
|
||||
- generic [ref=e243]:
|
||||
- heading "Color by" [level=4] [ref=e244]
|
||||
- combobox [ref=e245]:
|
||||
- generic: Mentioned
|
||||
- img [ref=e246]
|
||||
- generic [ref=e248]:
|
||||
- heading "Link types" [level=4] [ref=e249]
|
||||
- generic [ref=e252] [cursor=pointer]: semantic
|
||||
- generic [ref=e255] [cursor=pointer]: temporal
|
||||
- generic [ref=e258] [cursor=pointer]: entity
|
||||
- generic [ref=e261] [cursor=pointer]: causal
|
||||
- generic [ref=e262]:
|
||||
- generic [ref=e263]: "Nodes: 163"
|
||||
- generic [ref=e264]: "Links: 12563"
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,128 +0,0 @@
|
||||
- generic [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]
|
||||
- button "Experience" [ref=e70]
|
||||
- button "Observations" [ref=e71]: Observations
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e173]:
|
||||
- paragraph [ref=e174]: Consolidated knowledge synthesized from facts — patterns, preferences, and learnings that emerge from accumulated evidence.
|
||||
- generic [ref=e175]:
|
||||
- generic [ref=e177]:
|
||||
- generic [ref=e178]:
|
||||
- img
|
||||
- textbox "Filter by text or context (press Enter)..." [ref=e179]
|
||||
- generic [ref=e181]:
|
||||
- img
|
||||
- textbox "Filter by tag…" [ref=e182]
|
||||
- combobox "Scope" [active] [ref=e183]:
|
||||
- img
|
||||
- generic [ref=e184]:
|
||||
- generic [ref=e185]: "#eng"
|
||||
- generic [ref=e1724]: (8)
|
||||
- img
|
||||
- generic [ref=e186]:
|
||||
- generic [ref=e187]:
|
||||
- generic [ref=e188]: 8 total memories
|
||||
- 'generic "All memories consolidated (last: 6/11/2026, 5:58:22 PM)" [ref=e189]':
|
||||
- img [ref=e190]
|
||||
- text: In Sync
|
||||
- generic [ref=e193]:
|
||||
- button "Constellation" [ref=e194]:
|
||||
- img [ref=e195]
|
||||
- text: Constellation
|
||||
- button "Graph" [ref=e202]:
|
||||
- img [ref=e203]
|
||||
- text: Graph
|
||||
- button "Table" [ref=e208]:
|
||||
- img [ref=e209]
|
||||
- text: Table
|
||||
- button "Timeline" [ref=e210]:
|
||||
- img [ref=e211]
|
||||
- text: Timeline
|
||||
- generic [ref=e213]:
|
||||
- generic [ref=e217]:
|
||||
- button "Share" [ref=e218] [cursor=pointer]:
|
||||
- img [ref=e219]
|
||||
- text: Share
|
||||
- button "Fullscreen" [ref=e222] [cursor=pointer]:
|
||||
- img [ref=e223]
|
||||
- text: Fullscreen
|
||||
- button "Hide panel" [ref=e228]:
|
||||
- img [ref=e229]
|
||||
- generic [ref=e232]:
|
||||
- heading "Constellation View" [level=3] [ref=e233]
|
||||
- paragraph [ref=e234]: Canvas-rendered memory map with spatial label deconfliction. Scroll to zoom, drag to pan, hover to explore entity connections. Click a memory to view details.
|
||||
- generic [ref=e235]:
|
||||
- generic [ref=e236]:
|
||||
- img [ref=e237]
|
||||
- heading "Group by scope" [level=4] [ref=e241]
|
||||
- switch [ref=e242] [cursor=pointer]
|
||||
- generic [ref=e243]:
|
||||
- heading "Color by" [level=4] [ref=e244]
|
||||
- combobox [ref=e245]:
|
||||
- generic: Mentioned
|
||||
- img [ref=e246]
|
||||
- generic [ref=e248]:
|
||||
- heading "Link types" [level=4] [ref=e249]
|
||||
- generic [ref=e252] [cursor=pointer]: semantic
|
||||
- generic [ref=e255] [cursor=pointer]: temporal
|
||||
- generic [ref=e258] [cursor=pointer]: entity
|
||||
- generic [ref=e261] [cursor=pointer]: causal
|
||||
- generic [ref=e262]:
|
||||
- generic [ref=e263]: "Nodes: 8"
|
||||
- generic [ref=e264]: "Links: 62"
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,66 +0,0 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]: World Facts
|
||||
- button "Experience" [ref=e70]
|
||||
- button "ObservationsOff" [ref=e71]
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e74]:
|
||||
- paragraph [ref=e75]: Objective facts about the world received from external sources.
|
||||
- generic [ref=e77]:
|
||||
- img [ref=e78]
|
||||
- paragraph [ref=e83]: Loading memories...
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,113 +0,0 @@
|
||||
- generic [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]
|
||||
- button "Experience" [active] [ref=e70]: Experience
|
||||
- button "Observations" [ref=e94]
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e170]:
|
||||
- paragraph [ref=e171]: The bank's own actions, interactions, and first-person experiences.
|
||||
- generic [ref=e172]:
|
||||
- generic [ref=e174]:
|
||||
- generic [ref=e175]:
|
||||
- img
|
||||
- textbox "Filter by text or context (press Enter)..." [ref=e176]
|
||||
- generic [ref=e178]:
|
||||
- img
|
||||
- textbox "Filter by tag…" [ref=e179]
|
||||
- generic [ref=e180]:
|
||||
- generic [ref=e182]: 1 total memories
|
||||
- generic [ref=e183]:
|
||||
- button "Constellation" [ref=e184]:
|
||||
- img [ref=e185]
|
||||
- text: Constellation
|
||||
- button "Graph" [ref=e192]:
|
||||
- img [ref=e193]
|
||||
- text: Graph
|
||||
- button "Table" [ref=e198]:
|
||||
- img [ref=e199]
|
||||
- text: Table
|
||||
- button "Timeline" [ref=e200]:
|
||||
- img [ref=e201]
|
||||
- text: Timeline
|
||||
- generic [ref=e203]:
|
||||
- generic [ref=e207]:
|
||||
- button "Share" [ref=e208] [cursor=pointer]:
|
||||
- img [ref=e209]
|
||||
- text: Share
|
||||
- button "Fullscreen" [ref=e212] [cursor=pointer]:
|
||||
- img [ref=e213]
|
||||
- text: Fullscreen
|
||||
- button "Hide panel" [ref=e218]:
|
||||
- img [ref=e219]
|
||||
- generic [ref=e222]:
|
||||
- heading "Constellation View" [level=3] [ref=e223]
|
||||
- paragraph [ref=e224]: Canvas-rendered memory map with spatial label deconfliction. Scroll to zoom, drag to pan, hover to explore entity connections. Click a memory to view details.
|
||||
- generic [ref=e225]:
|
||||
- heading "Color by" [level=4] [ref=e226]
|
||||
- combobox [ref=e227]:
|
||||
- generic: Mentioned
|
||||
- img [ref=e228]
|
||||
- generic [ref=e230]:
|
||||
- heading "Link types" [level=4] [ref=e231]
|
||||
- generic [ref=e234] [cursor=pointer]: semantic
|
||||
- generic [ref=e237] [cursor=pointer]: temporal
|
||||
- generic [ref=e240] [cursor=pointer]: entity
|
||||
- generic [ref=e243] [cursor=pointer]: causal
|
||||
- generic [ref=e244]:
|
||||
- generic [ref=e245]: "Nodes: 1"
|
||||
- generic [ref=e246]: "Links: 1"
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
@@ -1,126 +0,0 @@
|
||||
- generic [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e4]:
|
||||
- img "Hindsight" [ref=e5]
|
||||
- combobox [ref=e7]:
|
||||
- generic [ref=e8]: cluster-demo
|
||||
- img
|
||||
- button "Add Document" [ref=e10]:
|
||||
- img
|
||||
- generic [ref=e11]: Add Document
|
||||
- link "GitHub" [ref=e12] [cursor=pointer]:
|
||||
- /url: https://github.com/vectorize-io/hindsight
|
||||
- img [ref=e13]
|
||||
- generic [ref=e16]: GitHub
|
||||
- button "Switch to dark mode" [ref=e18]:
|
||||
- img
|
||||
- button "Change language" [ref=e19]:
|
||||
- img
|
||||
- generic [ref=e20]:
|
||||
- complementary [ref=e21]:
|
||||
- navigation [ref=e22]:
|
||||
- list [ref=e23]:
|
||||
- listitem [ref=e24]:
|
||||
- link "Memories" [ref=e25] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=data
|
||||
- img [ref=e26]
|
||||
- listitem [ref=e30]:
|
||||
- link "Recall" [ref=e31] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=recall
|
||||
- img [ref=e32]
|
||||
- listitem [ref=e35]:
|
||||
- link "Reflect" [ref=e36] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=reflect
|
||||
- img [ref=e37]
|
||||
- listitem [ref=e40]:
|
||||
- link "Documents" [ref=e41] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=documents
|
||||
- img [ref=e42]
|
||||
- listitem [ref=e45]:
|
||||
- link "Entities" [ref=e46] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=entities
|
||||
- img [ref=e47]
|
||||
- listitem [ref=e52]:
|
||||
- link "Bank Configuration" [ref=e53] [cursor=pointer]:
|
||||
- /url: /banks/cluster-demo?view=profile
|
||||
- img [ref=e54]
|
||||
- button "Expand sidebar" [ref=e58]:
|
||||
- img [ref=e59]
|
||||
- main [ref=e61]:
|
||||
- generic [ref=e63]:
|
||||
- heading "Memories" [level=1] [ref=e64]
|
||||
- paragraph [ref=e65]: View and explore different types of memories stored in this memory bank.
|
||||
- generic [ref=e67]:
|
||||
- button "World Facts" [ref=e68]
|
||||
- button "Experience" [ref=e70]
|
||||
- button "Observations" [active] [ref=e94]: Observations
|
||||
- button "Mental Models" [ref=e72]
|
||||
- generic [ref=e248]:
|
||||
- paragraph [ref=e249]: Consolidated knowledge synthesized from facts — patterns, preferences, and learnings that emerge from accumulated evidence.
|
||||
- generic [ref=e250]:
|
||||
- generic [ref=e252]:
|
||||
- generic [ref=e253]:
|
||||
- img
|
||||
- textbox "Filter by text or context (press Enter)..." [ref=e254]
|
||||
- generic [ref=e256]:
|
||||
- img
|
||||
- textbox "Filter by tag…" [ref=e257]
|
||||
- combobox "Scope" [ref=e258]:
|
||||
- img
|
||||
- generic [ref=e260]: All scopes
|
||||
- img
|
||||
- generic [ref=e261]:
|
||||
- generic [ref=e262]:
|
||||
- generic [ref=e263]: 163 total memories
|
||||
- 'generic "All memories consolidated (last: 6/11/2026, 5:58:22 PM)" [ref=e264]':
|
||||
- img [ref=e265]
|
||||
- text: In Sync
|
||||
- generic [ref=e268]:
|
||||
- button "Constellation" [ref=e269]:
|
||||
- img [ref=e270]
|
||||
- text: Constellation
|
||||
- button "Graph" [ref=e277]:
|
||||
- img [ref=e278]
|
||||
- text: Graph
|
||||
- button "Table" [ref=e283]:
|
||||
- img [ref=e284]
|
||||
- text: Table
|
||||
- button "Timeline" [ref=e285]:
|
||||
- img [ref=e286]
|
||||
- text: Timeline
|
||||
- generic [ref=e288]:
|
||||
- generic [ref=e292]:
|
||||
- button "Share" [ref=e293] [cursor=pointer]:
|
||||
- img [ref=e294]
|
||||
- text: Share
|
||||
- button "Fullscreen" [ref=e297] [cursor=pointer]:
|
||||
- img [ref=e298]
|
||||
- text: Fullscreen
|
||||
- button "Hide panel" [ref=e303]:
|
||||
- img [ref=e304]
|
||||
- generic [ref=e307]:
|
||||
- heading "Constellation View" [level=3] [ref=e308]
|
||||
- paragraph [ref=e309]: Canvas-rendered memory map with spatial label deconfliction. Scroll to zoom, drag to pan, hover to explore entity connections. Click a memory to view details.
|
||||
- generic [ref=e310]:
|
||||
- generic [ref=e311]:
|
||||
- img [ref=e312]
|
||||
- heading "Group by scope" [level=4] [ref=e316]
|
||||
- switch [ref=e317] [cursor=pointer]
|
||||
- generic [ref=e318]:
|
||||
- heading "Color by" [level=4] [ref=e319]
|
||||
- combobox [ref=e320]:
|
||||
- generic: Mentioned
|
||||
- img [ref=e321]
|
||||
- generic [ref=e323]:
|
||||
- heading "Link types" [level=4] [ref=e324]
|
||||
- generic [ref=e327] [cursor=pointer]: semantic
|
||||
- generic [ref=e330] [cursor=pointer]: temporal
|
||||
- generic [ref=e333] [cursor=pointer]: entity
|
||||
- generic [ref=e336] [cursor=pointer]: causal
|
||||
- generic [ref=e337]:
|
||||
- generic [ref=e338]: "Nodes: 163"
|
||||
- generic [ref=e339]: "Links: 12563"
|
||||
- region "Notifications alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e89] [cursor=pointer]:
|
||||
- img [ref=e90]
|
||||
- alert [ref=e93]
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB |
@@ -250,7 +250,7 @@ Recall performs 4 retrieval strategies in parallel:
|
||||
- Graph: Entity/temporal/causal links
|
||||
- Temporal: Time range filtering
|
||||
|
||||

|
||||

|
||||
|
||||
The individual results from the retrievals are merged, then ordered by relevance using reciprocal rank fusion and a cross-encoder reranking model.
|
||||
|
||||
@@ -276,7 +276,7 @@ client = Hindsight(base_url="http://localhost:8888")
|
||||
client.reflect(bank_id="my-bank", query="What should I know about Alice?")
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 111 KiB |
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: hindsight
|
||||
description: Hindsight helm chart
|
||||
type: application
|
||||
version: 0.8.3
|
||||
appVersion: "0.8.3"
|
||||
version: 0.8.4
|
||||
appVersion: "0.8.4"
|
||||
keywords:
|
||||
- ai
|
||||
- memory
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vectorize-io/hindsight-all",
|
||||
"version": "0.8.3",
|
||||
"version": "0.8.4",
|
||||
"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,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hindsight-all-slim"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
description = "Hindsight: Agent Memory That Works Like Human Memory - Slim All-in-One Bundle"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"hindsight-api-slim==0.8.3",
|
||||
"hindsight-api-slim==0.8.4",
|
||||
"hindsight-client>=0.0.7",
|
||||
"hindsight-embed>=0.1.0",
|
||||
]
|
||||
|
||||
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-all"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
description = "Hindsight: Agent Memory That Works Like Human Memory - All-in-One Bundle"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"hindsight-api-slim[all]==0.8.3",
|
||||
"hindsight-api-slim[all]==0.8.4",
|
||||
"hindsight-client>=0.0.7",
|
||||
"hindsight-embed>=0.1.0",
|
||||
]
|
||||
@@ -21,7 +21,7 @@ hindsight-embed = { workspace = true }
|
||||
|
||||
[project.optional-dependencies]
|
||||
local-llm = [
|
||||
"hindsight-api-slim[local-llm]==0.8.3",
|
||||
"hindsight-api-slim[local-llm]==0.8.4",
|
||||
]
|
||||
test = [
|
||||
"pytest>=7.0.0",
|
||||
|
||||
@@ -121,7 +121,7 @@ This runs a stdio-based MCP server that can be used directly with MCP-compatible
|
||||
- **Entity Graph** — Automatic entity extraction and relationship tracking
|
||||
- **Temporal Reasoning** — Native support for time-based queries
|
||||
- **Disposition Traits** — Configurable skepticism, literalism, and empathy influence opinion formation
|
||||
- **Three Memory Types** — World facts, bank actions, and formed opinions with confidence scores
|
||||
- **Three Memory Types** — World facts, experience facts (the bank's own actions), and observations
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -53,4 +53,4 @@ __all__ = [
|
||||
"RemoteTEICrossEncoder",
|
||||
"LLMConfig",
|
||||
]
|
||||
__version__ = "0.8.3"
|
||||
__version__ = "0.8.4"
|
||||
|
||||
@@ -56,6 +56,7 @@ BACKUP_TABLES = [
|
||||
"observation_history",
|
||||
"mental_models",
|
||||
"mental_model_history",
|
||||
"knowledge_pages",
|
||||
"directives",
|
||||
"async_operations",
|
||||
"webhooks",
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
"""Add managed flag to knowledge_pages.
|
||||
|
||||
The knowledge base is managed by clients (CRUD over folders/pages). ``managed``
|
||||
lets a client tag a node as system-owned vs. hand-authored; it carries no
|
||||
server-side behaviour.
|
||||
|
||||
Revision ID: a5b6c7d8e9f0
|
||||
Revises: a9b8c7d6e5f4
|
||||
Create Date: 2026-06-26
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
from hindsight_api.alembic._dialect import run_for_dialect
|
||||
|
||||
revision: str = "a5b6c7d8e9f0"
|
||||
down_revision: str | Sequence[str] | None = "a9b8c7d6e5f4"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _pg_schema_prefix() -> str:
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def _pg_upgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
op.execute(f"ALTER TABLE {schema}knowledge_pages ADD COLUMN IF NOT EXISTS managed BOOLEAN NOT NULL DEFAULT false")
|
||||
|
||||
|
||||
def _pg_downgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
op.execute(f"ALTER TABLE {schema}knowledge_pages DROP COLUMN IF EXISTS managed")
|
||||
|
||||
|
||||
def _oracle_upgrade() -> None:
|
||||
op.execute("ALTER TABLE knowledge_pages ADD (managed NUMBER(1) DEFAULT 0 NOT NULL)")
|
||||
|
||||
|
||||
def _oracle_downgrade() -> None:
|
||||
op.execute("ALTER TABLE knowledge_pages DROP COLUMN managed")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_for_dialect(pg=_pg_upgrade, oracle=_oracle_upgrade)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
run_for_dialect(pg=_pg_downgrade, oracle=_oracle_downgrade)
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
"""Add knowledge_pages table (knowledge-base hierarchy).
|
||||
|
||||
The knowledge base organizes synthesized mental models into a navigable tree of
|
||||
**folders** and **pages**. A page references the mental model that holds its
|
||||
content (``mental_model_id``); a folder is a pure container (``mental_model_id``
|
||||
NULL). Hierarchy is a single self-referential ``parent_id`` so folders can nest
|
||||
arbitrarily. Content stays in ``mental_models`` — this table is metadata + tree
|
||||
structure only.
|
||||
|
||||
Revision ID: a9b8c7d6e5f4
|
||||
Revises: b57a7c9e0d13
|
||||
Create Date: 2026-06-25
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
from hindsight_api.alembic._dialect import run_for_dialect
|
||||
|
||||
revision: str = "a9b8c7d6e5f4"
|
||||
down_revision: str | Sequence[str] | None = "b57a7c9e0d13"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _pg_schema_prefix() -> str:
|
||||
"""Schema-qualifier for raw SQL on PG (multi-tenant search_path)."""
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def _pg_upgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
# parent_id self-FK cascades so deleting a folder row removes its whole
|
||||
# subtree of rows in one shot. The mental_model FK is composite (matches the
|
||||
# mental_models (id, bank_id) PK) and cascades too, so deleting a page's
|
||||
# mental model removes the page row — folders skip the FK because a NULL
|
||||
# column in a composite FK is not enforced (MATCH SIMPLE).
|
||||
op.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {schema}knowledge_pages (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
bank_id TEXT NOT NULL,
|
||||
parent_id VARCHAR(64),
|
||||
kind VARCHAR(16) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
mental_model_id VARCHAR(64),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT pk_knowledge_pages PRIMARY KEY (id),
|
||||
CONSTRAINT ck_knowledge_pages_kind CHECK (kind IN ('folder', 'page')),
|
||||
CONSTRAINT fk_kp_bank FOREIGN KEY (bank_id)
|
||||
REFERENCES {schema}banks(bank_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_kp_parent FOREIGN KEY (parent_id)
|
||||
REFERENCES {schema}knowledge_pages(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_kp_mm FOREIGN KEY (mental_model_id, bank_id)
|
||||
REFERENCES {schema}mental_models(id, bank_id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE INDEX IF NOT EXISTS idx_kp_bank_parent ON {schema}knowledge_pages (bank_id, parent_id, sort_order)"
|
||||
)
|
||||
|
||||
|
||||
def _pg_downgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
op.execute(f"DROP INDEX IF EXISTS {schema}idx_kp_bank_parent")
|
||||
op.execute(f"DROP TABLE IF EXISTS {schema}knowledge_pages")
|
||||
|
||||
|
||||
def _oracle_upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS knowledge_pages (
|
||||
id VARCHAR2(64) NOT NULL,
|
||||
bank_id VARCHAR2(256) NOT NULL,
|
||||
parent_id VARCHAR2(64),
|
||||
kind VARCHAR2(16) NOT NULL,
|
||||
name CLOB NOT NULL,
|
||||
mental_model_id VARCHAR2(64),
|
||||
sort_order NUMBER DEFAULT 0 NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT pk_knowledge_pages PRIMARY KEY (id),
|
||||
CONSTRAINT ck_knowledge_pages_kind CHECK (kind IN ('folder', 'page')),
|
||||
CONSTRAINT fk_kp_bank FOREIGN KEY (bank_id)
|
||||
REFERENCES banks(bank_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_kp_parent FOREIGN KEY (parent_id)
|
||||
REFERENCES knowledge_pages(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_kp_mm FOREIGN KEY (mental_model_id, bank_id)
|
||||
REFERENCES mental_models(id, bank_id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX idx_kp_bank_parent ON knowledge_pages (bank_id, parent_id, sort_order)")
|
||||
|
||||
|
||||
def _oracle_downgrade() -> None:
|
||||
op.execute("DROP TABLE knowledge_pages CASCADE CONSTRAINTS")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_for_dialect(pg=_pg_upgrade, oracle=_oracle_upgrade)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
run_for_dialect(pg=_pg_downgrade, oracle=_oracle_downgrade)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
"""Add bank_stats_cache table for distributed get_bank_stats caching
|
||||
|
||||
Revision ID: b57a7c9e0d13
|
||||
Revises: c3f7a1b9d2e4
|
||||
Create Date: 2026-07-01
|
||||
|
||||
get_bank_stats aggregates over memory_links / unit_entities — a multi-second scan
|
||||
on banks with millions of rows. The result was cached per-process (in-memory), so
|
||||
every API worker recomputed it once per TTL and the first caller after expiry
|
||||
stalled. This table backs a shared, cross-process TTL cache: one worker's compute
|
||||
is written here and served to all the others.
|
||||
|
||||
PostgreSQL only. Oracle keeps the in-process cache (the runtime picks the backing
|
||||
store by dialect), so the Oracle upgrade slot is intentionally absent.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
from hindsight_api.alembic._dialect import run_for_dialect
|
||||
|
||||
revision: str = "b57a7c9e0d13"
|
||||
down_revision: str | Sequence[str] | None = "c3f7a1b9d2e4"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _get_schema_prefix() -> str:
|
||||
"""Schema-qualifier for raw SQL on PG (multi-tenant search_path)."""
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def _pg_upgrade() -> None:
|
||||
schema = _get_schema_prefix()
|
||||
# One row per bank: payload is the full get_bank_stats result, computed_at
|
||||
# drives logical TTL expiry. Rows are overwritten in place (ON CONFLICT), so
|
||||
# the table never grows beyond the number of banks and needs no purge job.
|
||||
op.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {schema}bank_stats_cache (
|
||||
bank_id TEXT PRIMARY KEY,
|
||||
payload JSONB NOT NULL,
|
||||
computed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _pg_downgrade() -> None:
|
||||
schema = _get_schema_prefix()
|
||||
op.execute(f"DROP TABLE IF EXISTS {schema}bank_stats_cache")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_for_dialect(pg=_pg_upgrade) # oracle slot intentionally absent → no-op
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
run_for_dialect(pg=_pg_downgrade)
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
"""Unique page name per folder in knowledge_pages.
|
||||
|
||||
The folder curator can fire concurrently (folder-create trigger + the
|
||||
post-consolidation sweep), and an in-process lock can't serialize runs that
|
||||
execute in different threads/loops. A partial unique index on
|
||||
(bank_id, parent, lower(name)) for pages makes duplicate-named pages in the same
|
||||
folder impossible at the DB level — the second concurrent insert fails and the
|
||||
curator treats it as "already exists".
|
||||
|
||||
PostgreSQL only: the Oracle ``name`` column is a CLOB and cannot back a
|
||||
functional unique index; Oracle relies on the in-process serialization instead.
|
||||
|
||||
Revision ID: c3d4e5f6a7b8
|
||||
Revises: a5b6c7d8e9f0
|
||||
Create Date: 2026-06-26
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
from hindsight_api.alembic._dialect import run_for_dialect
|
||||
|
||||
revision: str = "c3d4e5f6a7b8"
|
||||
down_revision: str | Sequence[str] | None = "a5b6c7d8e9f0"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _pg_schema_prefix() -> str:
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def _pg_upgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
# First drop any pre-existing duplicate pages (created by the racy curator
|
||||
# before this guard existed), keeping the earliest row of each duplicate set,
|
||||
# so the unique index can be built. Their backing mental models are left in
|
||||
# place (harmless orphans).
|
||||
op.execute(
|
||||
f"""
|
||||
DELETE FROM {schema}knowledge_pages a
|
||||
USING {schema}knowledge_pages b
|
||||
WHERE a.kind = 'page' AND b.kind = 'page'
|
||||
AND a.bank_id = b.bank_id
|
||||
AND COALESCE(a.parent_id, '') = COALESCE(b.parent_id, '')
|
||||
AND lower(a.name) = lower(b.name)
|
||||
AND a.ctid > b.ctid
|
||||
"""
|
||||
)
|
||||
# COALESCE(parent_id, '') so root-level pages (NULL parent) are also unique by
|
||||
# name — NULLs would otherwise compare distinct and allow duplicates.
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_kp_folder_pagename "
|
||||
f"ON {schema}knowledge_pages (bank_id, COALESCE(parent_id, ''), lower(name)) "
|
||||
"WHERE kind = 'page'"
|
||||
)
|
||||
|
||||
|
||||
def _pg_downgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
op.execute(f"DROP INDEX IF EXISTS {schema}uq_kp_folder_pagename")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_for_dialect(pg=_pg_upgrade) # oracle slot intentionally absent (CLOB name)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
run_for_dialect(pg=_pg_downgrade)
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
"""Backfill search_vector for native-backend observations.
|
||||
|
||||
Observations created or updated by the consolidator landed with a NULL
|
||||
``search_vector`` under the ``native`` text-search backend: the
|
||||
single-row INSERT/UPDATE paths in ``consolidator.py`` never populated the
|
||||
tsvector (only the batch raw-fact path in ``ops_postgresql.insert_facts_batch``
|
||||
did). Those observations were therefore invisible to the BM25 retrieval arm
|
||||
until they were re-written by a later consolidation pass. The writer is fixed
|
||||
in the same change set (all four consolidator sites now call
|
||||
``to_tsvector($lang, COALESCE(text, ''))``); this migration repairs the
|
||||
historical residue so existing observations become BM25-searchable without a
|
||||
re-ingest.
|
||||
|
||||
Scope mirrors the writer fix exactly:
|
||||
* Only the ``native`` backend is touched. The gate is the column *type*:
|
||||
under ``native`` ``search_vector`` is a regular (non-generated) tsvector
|
||||
column; under ``vchord`` it is a ``bm25vector`` and under
|
||||
``pg_textsearch`` / ``pgroonga`` / ``pg_search`` it is a dummy ``text``
|
||||
column. ``_is_regular_tsvector`` is true only for ``native``, so every
|
||||
other backend is a no-op.
|
||||
* The tsvector is built from the observation's own ``text`` only — matching
|
||||
the consolidator INSERT/UPDATE paths (entity / source / temporal signals
|
||||
are intentionally excluded; the other retrieval arms cover those).
|
||||
* Only ``fact_type = 'observation'`` rows with a NULL ``search_vector`` are
|
||||
rewritten. Raw facts already carry a populated tsvector, and the
|
||||
``IS NULL`` predicate makes the migration idempotent and re-runnable.
|
||||
|
||||
The configured ``HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE`` is used
|
||||
so backfilled rows are lexically identical to newly-created observations. The
|
||||
value is validated as a PG identifier (mirroring
|
||||
``HindsightConfig.validate``) before being embedded as a SQL literal.
|
||||
|
||||
This is a single UPDATE per schema: it locks the targeted observation rows for
|
||||
its duration. It is one-time and only touches unpopulated rows, so subsequent
|
||||
online writes (which now carry the tsvector via the writer fix) are unaffected.
|
||||
|
||||
Oracle slot is intentionally absent: the consolidator INSERT/UPDATE paths that
|
||||
this repairs are PostgreSQL-specific (``ops_postgresql``), and the native
|
||||
tsvector ``search_vector`` column only exists on PostgreSQL. There is no Oracle
|
||||
residue to repair.
|
||||
|
||||
Revision ID: c3f7a1b9d2e4
|
||||
Revises: f4d1c2b3a5e6
|
||||
Create Date: 2026-06-29
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
from sqlalchemy import Connection, text
|
||||
|
||||
from hindsight_api.alembic._dialect import run_for_dialect
|
||||
from hindsight_api.config import (
|
||||
DEFAULT_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE,
|
||||
ENV_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE,
|
||||
)
|
||||
|
||||
revision: str = "c3f7a1b9d2e4"
|
||||
down_revision: str | Sequence[str] | None = "f4d1c2b3a5e6"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
# Matches HindsightConfig.validate(): a tsvector regconfig name embedded as a
|
||||
# SQL literal must be a bare PG identifier.
|
||||
_PG_IDENTIFIER = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*")
|
||||
|
||||
|
||||
def _schema_prefix() -> str:
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def _schema_name() -> str:
|
||||
return (context.config.get_main_option("target_schema") or "public").strip('"')
|
||||
|
||||
|
||||
def _native_language() -> str:
|
||||
"""Configured native tsvector language, validated as a PG identifier."""
|
||||
lang = os.getenv(
|
||||
ENV_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE,
|
||||
DEFAULT_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE,
|
||||
)
|
||||
if not _PG_IDENTIFIER.fullmatch(lang):
|
||||
return DEFAULT_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE
|
||||
return lang
|
||||
|
||||
|
||||
def _is_regular_tsvector(conn: Connection, schema: str, table: str) -> bool:
|
||||
"""True iff ``schema.table.search_vector`` is a non-generated tsvector column.
|
||||
|
||||
This is the ``native`` backend signature. ``vchord`` (bm25vector) and
|
||||
``pg_textsearch`` / ``pgroonga`` / ``pg_search`` (dummy text column) all
|
||||
fail this check, so the backfill is a no-op for them.
|
||||
"""
|
||||
row = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT is_generated, udt_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = :schema
|
||||
AND table_name = :table
|
||||
AND column_name = 'search_vector'
|
||||
"""
|
||||
),
|
||||
{"schema": schema, "table": table},
|
||||
).fetchone()
|
||||
if not row:
|
||||
return False
|
||||
is_generated, udt_name = row[0], row[1]
|
||||
return udt_name == "tsvector" and is_generated != "ALWAYS"
|
||||
|
||||
|
||||
def _pg_upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
schema_name = _schema_name()
|
||||
if not _is_regular_tsvector(conn, schema_name, "memory_units"):
|
||||
# Non-native backend (or column absent) — nothing to backfill.
|
||||
return
|
||||
schema_prefix = _schema_prefix()
|
||||
lang = _native_language()
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE {schema_prefix}memory_units
|
||||
SET search_vector = to_tsvector('{lang}'::regconfig, COALESCE(text, ''))
|
||||
WHERE fact_type = 'observation' AND search_vector IS NULL
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _pg_downgrade() -> None:
|
||||
# No-op: backfilled rows are indistinguishable from observations that were
|
||||
# populated by the post-fix writer, and reverting either to NULL would
|
||||
# re-break BM25 retrieval. The column simply stays populated.
|
||||
pass
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_for_dialect(pg=_pg_upgrade)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
run_for_dialect(pg=_pg_downgrade)
|
||||
@@ -18,6 +18,7 @@ from typing import Any, Literal, TypeVar
|
||||
from fastapi import Depends, FastAPI, File, Form, Header, HTTPException, Query, Request, UploadFile
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
from hindsight_api.api import okf
|
||||
from hindsight_api.api.disconnect import ClientDisconnectCancellationMiddleware, get_scope_cancellation_token
|
||||
from hindsight_api.cancellation import OperationCancelledError
|
||||
from hindsight_api.engine.audit import (
|
||||
@@ -27,7 +28,7 @@ from hindsight_api.engine.audit import (
|
||||
AuditLogStatsResponse,
|
||||
)
|
||||
from hindsight_api.engine.llm_trace import LLMRequestListResponse, LLMRequestStatsResponse
|
||||
from hindsight_api.extensions import AuthenticationError
|
||||
from hindsight_api.extensions import AuthenticationError, PrecheckOperation
|
||||
|
||||
|
||||
def _parse_metadata(metadata: Any) -> dict[str, Any]:
|
||||
@@ -2110,6 +2111,150 @@ class MentalModelListResponse(BaseModel):
|
||||
items: list[MentalModelResponse]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# KNOWLEDGE BASE (folders + pages over mental models, projected to OKF)
|
||||
# =========================================================================
|
||||
|
||||
|
||||
class KnowledgeNode(BaseModel):
|
||||
"""A node in the knowledge-base tree — a folder or a page.
|
||||
|
||||
Pages carry ``description``/``tags`` from their backing mental model. The
|
||||
knowledge base is client-managed (CRUD); ``managed`` lets a client tag a node
|
||||
as system-owned vs. hand-authored.
|
||||
"""
|
||||
|
||||
id: str
|
||||
kind: Literal["folder", "page"]
|
||||
name: str
|
||||
parent_id: str | None = None
|
||||
mental_model_id: str | None = Field(default=None, description="Backing mental model id (pages only).")
|
||||
managed: bool = Field(default=False, description="Client-set flag: true = system-owned, false = hand-authored.")
|
||||
description: str | None = Field(default=None, description="Page source query (OKF `description`).")
|
||||
tags: list[str] = FieldWithDefault(list)
|
||||
timestamp: str | None = Field(default=None, description="Last refresh (page) or last update (folder).")
|
||||
children: list["KnowledgeNode"] = FieldWithDefault(list)
|
||||
|
||||
|
||||
class KnowledgeTreeResponse(BaseModel):
|
||||
"""The knowledge base as a nested folder/page tree."""
|
||||
|
||||
roots: list[KnowledgeNode]
|
||||
|
||||
|
||||
class CreateFolderRequest(BaseModel):
|
||||
"""Create a folder under an optional parent folder."""
|
||||
|
||||
name: str
|
||||
parent_id: str | None = None
|
||||
|
||||
|
||||
class CreatePageRequest(BaseModel):
|
||||
"""Create a page (a mental model + tree node) under an optional parent folder."""
|
||||
|
||||
name: str
|
||||
source_query: str
|
||||
parent_id: str | None = None
|
||||
tags: list[str] | None = None
|
||||
max_tokens: int | None = None
|
||||
trigger: MentalModelTrigger | None = None
|
||||
|
||||
|
||||
class UpdateNodeRequest(BaseModel):
|
||||
"""Rename and/or move a node. Each field applies only when present."""
|
||||
|
||||
name: str | None = None
|
||||
parent_id: str | None = None
|
||||
|
||||
|
||||
class CreateKnowledgePageResponse(BaseModel):
|
||||
"""Result of creating a page: the node id, its mental model, and the refresh op."""
|
||||
|
||||
page_id: str
|
||||
mental_model_id: str
|
||||
operation_id: str | None = None
|
||||
|
||||
|
||||
class KnowledgePageResponse(BaseModel):
|
||||
"""A knowledge page rendered as an OKF document."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
type: str = Field(description="OKF document type — from a `type:<x>` tag, else 'knowledge-page'.")
|
||||
description: str | None = Field(default=None, description="The source query that rebuilds the page.")
|
||||
tags: list[str] = FieldWithDefault(list)
|
||||
timestamp: str | None = Field(default=None, description="Last refresh time (falls back to creation).")
|
||||
body: str | None = Field(default=None, description="The page's synthesized markdown body.")
|
||||
markdown: str = Field(description="The full OKF document: YAML frontmatter + markdown body.")
|
||||
|
||||
|
||||
class KnowledgePageGraphResponse(BaseModel):
|
||||
"""Constellation graph of knowledge pages linked by shared tags."""
|
||||
|
||||
nodes: list[dict[str, Any]]
|
||||
edges: list[dict[str, Any]]
|
||||
total_pages: int
|
||||
total_edges: int
|
||||
|
||||
|
||||
class KnowledgePageBundleFile(BaseModel):
|
||||
"""One file in a portable OKF bundle."""
|
||||
|
||||
path: str
|
||||
content: str
|
||||
|
||||
|
||||
class KnowledgePageBundleResponse(BaseModel):
|
||||
"""A portable OKF bundle — a flat set of markdown files (index + pages + logs)."""
|
||||
|
||||
files: list[KnowledgePageBundleFile]
|
||||
|
||||
|
||||
def _knowledge_node_model(node: dict[str, Any]) -> KnowledgeNode:
|
||||
"""Project an engine node dict into a (childless) KnowledgeNode."""
|
||||
is_page = node.get("kind") == "page"
|
||||
return KnowledgeNode(
|
||||
id=node["id"],
|
||||
kind=node["kind"],
|
||||
name=node["name"],
|
||||
parent_id=node.get("parent_id"),
|
||||
mental_model_id=node.get("mental_model_id"),
|
||||
managed=bool(node.get("managed")),
|
||||
description=node.get("source_query") if is_page else None,
|
||||
tags=list(node.get("tags") or []) if is_page else [],
|
||||
timestamp=(node.get("last_refreshed_at") if is_page else node.get("updated_at")),
|
||||
)
|
||||
|
||||
|
||||
def _build_knowledge_tree(nodes: list[dict[str, Any]]) -> list[KnowledgeNode]:
|
||||
"""Assemble the flat node list into a nested tree of roots."""
|
||||
models = {n["id"]: _knowledge_node_model(n) for n in nodes}
|
||||
roots: list[KnowledgeNode] = []
|
||||
for node in nodes:
|
||||
model = models[node["id"]]
|
||||
parent_id = node.get("parent_id")
|
||||
if parent_id and parent_id in models:
|
||||
models[parent_id].children.append(model)
|
||||
else:
|
||||
roots.append(model)
|
||||
return roots
|
||||
|
||||
|
||||
def _knowledge_page_response(node: dict[str, Any]) -> KnowledgePageResponse:
|
||||
"""Project a page node (with merged mental-model content) into an OKF document."""
|
||||
page = okf.page_type(node.get("tags"))
|
||||
return KnowledgePageResponse(
|
||||
id=node["id"],
|
||||
name=node["name"],
|
||||
type=page.type,
|
||||
description=node.get("source_query"),
|
||||
tags=page.display_tags,
|
||||
timestamp=node.get("last_refreshed_at") or node.get("created_at"),
|
||||
body=node.get("content"),
|
||||
markdown=okf.render_document(node),
|
||||
)
|
||||
|
||||
|
||||
class CreateMentalModelRequest(BaseModel):
|
||||
"""Request model for creating a mental model."""
|
||||
|
||||
@@ -3380,7 +3525,7 @@ def _register_routes(app: FastAPI):
|
||||
api_key = authorization.strip()
|
||||
return RequestContext(api_key=api_key)
|
||||
|
||||
def precheck_for(operation: str):
|
||||
def precheck_for(operation: PrecheckOperation):
|
||||
"""
|
||||
Build a FastAPI dependency that runs ``OperationValidator.precheck``.
|
||||
|
||||
@@ -3631,7 +3776,7 @@ def _register_routes(app: FastAPI):
|
||||
async def _require_dry_run_enabled() -> None:
|
||||
"""Feature-flag gate for dry-run extraction.
|
||||
|
||||
Declared as a dependency BEFORE ``precheck_for("dry_run_extract")`` so a
|
||||
Declared as a dependency BEFORE ``precheck_for(PrecheckOperation.DRY_RUN_EXTRACT)`` so a
|
||||
disabled route returns 404 regardless of tenant/billing state — FastAPI
|
||||
resolves path-operation dependencies in signature order, so this runs
|
||||
first and preserves the original "disabled → 404" contract.
|
||||
@@ -3661,7 +3806,7 @@ def _register_routes(app: FastAPI):
|
||||
body: DryRunExtractRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
_enabled: None = Depends(_require_dry_run_enabled),
|
||||
_precheck: None = Depends(precheck_for("dry_run_extract")),
|
||||
_precheck: None = Depends(precheck_for(PrecheckOperation.DRY_RUN_EXTRACT)),
|
||||
):
|
||||
try:
|
||||
override_fields = (
|
||||
@@ -3829,7 +3974,7 @@ def _register_routes(app: FastAPI):
|
||||
request: RecallRequest,
|
||||
http_request: Request,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
_precheck: None = Depends(precheck_for("recall")),
|
||||
_precheck: None = Depends(precheck_for(PrecheckOperation.RECALL)),
|
||||
):
|
||||
"""Run a recall and return results with trace."""
|
||||
import time
|
||||
@@ -4033,7 +4178,7 @@ def _register_routes(app: FastAPI):
|
||||
request: ReflectRequest,
|
||||
http_request: Request,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
_precheck: None = Depends(precheck_for("reflect")),
|
||||
_precheck: None = Depends(precheck_for(PrecheckOperation.REFLECT)),
|
||||
):
|
||||
metrics = get_metrics_collector()
|
||||
|
||||
@@ -4191,11 +4336,17 @@ def _register_routes(app: FastAPI):
|
||||
)
|
||||
async def api_stats(
|
||||
bank_id: str,
|
||||
refresh: bool = Query(
|
||||
default=False,
|
||||
description="Force a fresh recompute, bypassing the cached value (and refreshing the cache).",
|
||||
),
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Get statistics about memory nodes and links for a memory bank."""
|
||||
try:
|
||||
stats = await app.state.memory.get_bank_stats(bank_id, request_context=request_context)
|
||||
stats = await app.state.memory.get_bank_stats(
|
||||
bank_id, request_context=request_context, force_refresh=refresh
|
||||
)
|
||||
nodes_by_type = stats["node_counts"]
|
||||
links_by_type = stats["link_counts"]
|
||||
links_by_fact_type = stats["link_counts_by_fact_type"]
|
||||
@@ -4580,7 +4731,7 @@ def _register_routes(app: FastAPI):
|
||||
bank_id: str,
|
||||
body: CreateMentalModelRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
_precheck: None = Depends(precheck_for("mental_model_create")),
|
||||
_precheck: None = Depends(precheck_for(PrecheckOperation.MENTAL_MODEL_CREATE)),
|
||||
):
|
||||
"""Create a mental model (async - returns operation_id)."""
|
||||
try:
|
||||
@@ -4629,7 +4780,7 @@ def _register_routes(app: FastAPI):
|
||||
bank_id: str,
|
||||
mental_model_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
_precheck: None = Depends(precheck_for("mental_model_refresh")),
|
||||
_precheck: None = Depends(precheck_for(PrecheckOperation.MENTAL_MODEL_REFRESH)),
|
||||
):
|
||||
"""Refresh a mental model by re-running its source query (async)."""
|
||||
try:
|
||||
@@ -4775,6 +4926,333 @@ def _register_routes(app: FastAPI):
|
||||
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/mental-models/{mental_model_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# =========================================================================
|
||||
# KNOWLEDGE BASE ENDPOINTS (folders + pages, Open Knowledge Format)
|
||||
# =========================================================================
|
||||
# A hierarchy of folders and pages over mental models. Pages project to OKF
|
||||
# documents (markdown body + YAML frontmatter); see api/okf.py. The static
|
||||
# sub-paths (/tree, /folders, /pages, /graph, /export) are declared before
|
||||
# the /pages/{id} and /nodes/{id} path-parameter routes so they win.
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/tree",
|
||||
response_model=KnowledgeTreeResponse,
|
||||
summary="Get the knowledge-base tree",
|
||||
description="Return the knowledge base as a nested tree of folders and pages.",
|
||||
operation_id="get_knowledge_base_tree",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_knowledge_base_tree(
|
||||
bank_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Return the folder/page tree for a bank."""
|
||||
try:
|
||||
nodes = await app.state.memory.list_knowledge_nodes(bank_id=bank_id, request_context=request_context)
|
||||
return KnowledgeTreeResponse(roots=_build_knowledge_tree(nodes))
|
||||
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 GET /v1/default/banks/{bank_id}/knowledge-base/tree: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/folders",
|
||||
response_model=KnowledgeNode,
|
||||
status_code=201,
|
||||
summary="Create a knowledge-base folder",
|
||||
description="Create a folder, optionally nested under a parent folder.",
|
||||
operation_id="create_knowledge_folder",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_create_knowledge_folder(
|
||||
bank_id: str,
|
||||
body: CreateFolderRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Create a folder node."""
|
||||
try:
|
||||
node = await app.state.memory.create_knowledge_folder(
|
||||
bank_id=bank_id,
|
||||
name=body.name,
|
||||
parent_id=body.parent_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
return _knowledge_node_model(node)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
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 POST /v1/default/banks/{bank_id}/knowledge-base/folders: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/pages",
|
||||
response_model=CreateKnowledgePageResponse,
|
||||
status_code=201,
|
||||
summary="Create a knowledge-base page",
|
||||
description="Create a page (a mental model + tree node). Content is generated asynchronously; "
|
||||
"use the returned operation_id to track completion.",
|
||||
operation_id="create_knowledge_page",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_create_knowledge_page(
|
||||
bank_id: str,
|
||||
body: CreatePageRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Create a page node (async content generation)."""
|
||||
try:
|
||||
node = await app.state.memory.create_knowledge_page(
|
||||
bank_id=bank_id,
|
||||
name=body.name,
|
||||
source_query=body.source_query,
|
||||
content="Generating content...",
|
||||
parent_id=body.parent_id,
|
||||
tags=body.tags if body.tags else None,
|
||||
max_tokens=body.max_tokens,
|
||||
trigger=body.trigger.model_dump() if body.trigger else None,
|
||||
request_context=request_context,
|
||||
)
|
||||
if node is None:
|
||||
raise HTTPException(status_code=409, detail=f"A page named '{body.name}' already exists in this folder")
|
||||
result = await app.state.memory.submit_async_refresh_mental_model(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=node["mental_model_id"],
|
||||
request_context=request_context,
|
||||
)
|
||||
return CreateKnowledgePageResponse(
|
||||
page_id=node["id"],
|
||||
mental_model_id=node["mental_model_id"],
|
||||
operation_id=result["operation_id"],
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
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 POST /v1/default/banks/{bank_id}/knowledge-base/pages: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/graph",
|
||||
response_model=KnowledgePageGraphResponse,
|
||||
summary="Knowledge-base constellation graph",
|
||||
description="Return pages as nodes linked by shared tags, for the constellation view.",
|
||||
operation_id="get_knowledge_base_graph",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_knowledge_base_graph(
|
||||
bank_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Return the shared-tag constellation graph for a bank's pages."""
|
||||
try:
|
||||
nodes = await app.state.memory.list_knowledge_nodes(bank_id=bank_id, request_context=request_context)
|
||||
pages = [n for n in nodes if n.get("kind") == "page"]
|
||||
# Cluster the constellation by parent folder (the knowledge base's own
|
||||
# structure) rather than by the retired type: tag.
|
||||
folder_names = {n["id"]: n["name"] for n in nodes if n.get("kind") == "folder"}
|
||||
graph = okf.knowledge_graph(pages, cluster_for=lambda p: folder_names.get(p.get("parent_id"), "Ungrouped"))
|
||||
return KnowledgePageGraphResponse(
|
||||
nodes=graph.nodes,
|
||||
edges=graph.edges,
|
||||
total_pages=len(graph.nodes),
|
||||
total_edges=len(graph.edges),
|
||||
)
|
||||
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 GET /v1/default/banks/{bank_id}/knowledge-base/graph: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/export",
|
||||
response_model=KnowledgePageBundleResponse,
|
||||
summary="Export the knowledge base as an OKF bundle",
|
||||
description="Return a portable OKF bundle: a nested index.md, one <id>.md per page, and history logs.",
|
||||
operation_id="export_knowledge_base",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_export_knowledge_base(
|
||||
bank_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Export a bank's knowledge base as a flat OKF markdown bundle."""
|
||||
try:
|
||||
nodes = await app.state.memory.list_knowledge_nodes(bank_id=bank_id, request_context=request_context)
|
||||
files = [KnowledgePageBundleFile(path=okf.INDEX_FILENAME, content=okf.render_index(nodes))]
|
||||
for node in nodes:
|
||||
if node.get("kind") != "page":
|
||||
continue
|
||||
page = await app.state.memory.get_knowledge_page(
|
||||
bank_id=bank_id, page_id=node["id"], request_context=request_context
|
||||
)
|
||||
if page is None:
|
||||
continue
|
||||
files.append(
|
||||
KnowledgePageBundleFile(path=okf.page_filename(node["id"]), content=okf.render_document(page))
|
||||
)
|
||||
if node.get("mental_model_id"):
|
||||
history = (
|
||||
await app.state.memory.get_mental_model_history(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=node["mental_model_id"],
|
||||
request_context=request_context,
|
||||
)
|
||||
or []
|
||||
)
|
||||
if history:
|
||||
files.append(
|
||||
KnowledgePageBundleFile(
|
||||
path=okf.log_filename(node["id"]), content=okf.render_log(page, history)
|
||||
)
|
||||
)
|
||||
return KnowledgePageBundleResponse(files=files)
|
||||
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 GET /v1/default/banks/{bank_id}/knowledge-base/export: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}",
|
||||
response_model=KnowledgePageResponse,
|
||||
summary="Get a knowledge-base page",
|
||||
description="Return a single page as an OKF document (frontmatter + markdown body).",
|
||||
operation_id="get_knowledge_page",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_get_knowledge_page(
|
||||
bank_id: str,
|
||||
page_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Get a single page as an OKF document."""
|
||||
try:
|
||||
node = await app.state.memory.get_knowledge_page(
|
||||
bank_id=bank_id, page_id=page_id, request_context=request_context
|
||||
)
|
||||
if node is None:
|
||||
raise HTTPException(status_code=404, detail=f"Knowledge page '{page_id}' not found")
|
||||
return _knowledge_page_response(node)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in GET /v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.patch(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}",
|
||||
response_model=KnowledgeNode,
|
||||
summary="Rename or move a knowledge-base node",
|
||||
description="Rename a node (set `name`) and/or move it under another folder (set `parent_id`, "
|
||||
"null for the root).",
|
||||
operation_id="update_knowledge_node",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_update_knowledge_node(
|
||||
bank_id: str,
|
||||
node_id: str,
|
||||
body: UpdateNodeRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Rename and/or move a node."""
|
||||
try:
|
||||
updated: dict[str, Any] | None = None
|
||||
did_change = False
|
||||
if body.name is not None:
|
||||
did_change = True
|
||||
updated = await app.state.memory.rename_knowledge_node(
|
||||
bank_id=bank_id, node_id=node_id, name=body.name, request_context=request_context
|
||||
)
|
||||
# parent_id is applied only when present in the body, so passing null
|
||||
# moves the node to the root (distinct from "not provided").
|
||||
if "parent_id" in body.model_fields_set:
|
||||
did_change = True
|
||||
updated = await app.state.memory.move_knowledge_node(
|
||||
bank_id=bank_id, node_id=node_id, new_parent_id=body.parent_id, request_context=request_context
|
||||
)
|
||||
if not did_change:
|
||||
raise HTTPException(status_code=400, detail="Provide name and/or parent_id to update")
|
||||
if updated is None:
|
||||
raise HTTPException(status_code=404, detail=f"Knowledge node '{node_id}' not found")
|
||||
return _knowledge_node_model(updated)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.delete(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}",
|
||||
summary="Delete a knowledge-base node",
|
||||
description="Delete a folder or page and its whole subtree (pages' mental models are removed too).",
|
||||
operation_id="delete_knowledge_node",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_delete_knowledge_node(
|
||||
bank_id: str,
|
||||
node_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Delete a node and its subtree."""
|
||||
try:
|
||||
deleted = await app.state.memory.delete_knowledge_node(
|
||||
bank_id=bank_id, node_id=node_id, request_context=request_context
|
||||
)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail=f"Knowledge node '{node_id}' not found")
|
||||
return {"status": "deleted"}
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# =========================================================================
|
||||
# DIRECTIVES ENDPOINTS
|
||||
# =========================================================================
|
||||
@@ -6215,9 +6693,11 @@ def _register_routes(app: FastAPI):
|
||||
# Authenticate and set schema context for multi-tenant DB queries
|
||||
await app.state.memory._authenticate_tenant(request_context)
|
||||
if app.state.memory._operation_validator:
|
||||
from hindsight_api.extensions import BankReadContext
|
||||
from hindsight_api.extensions import BankReadContext, BankReadOperation
|
||||
|
||||
ctx = BankReadContext(bank_id=bank_id, operation="get_bank_config", request_context=request_context)
|
||||
ctx = BankReadContext(
|
||||
bank_id=bank_id, operation=BankReadOperation.GET_BANK_CONFIG, request_context=request_context
|
||||
)
|
||||
await app.state.memory._validate_operation(
|
||||
app.state.memory._operation_validator.validate_bank_read(ctx)
|
||||
)
|
||||
@@ -6263,9 +6743,11 @@ def _register_routes(app: FastAPI):
|
||||
# Authenticate and set schema context for multi-tenant DB queries
|
||||
await app.state.memory._authenticate_tenant(request_context)
|
||||
if app.state.memory._operation_validator:
|
||||
from hindsight_api.extensions import BankWriteContext
|
||||
from hindsight_api.extensions import BankWriteContext, BankWriteOperation
|
||||
|
||||
ctx = BankWriteContext(bank_id=bank_id, operation="update_bank_config", request_context=request_context)
|
||||
ctx = BankWriteContext(
|
||||
bank_id=bank_id, operation=BankWriteOperation.UPDATE_BANK_CONFIG, request_context=request_context
|
||||
)
|
||||
await app.state.memory._validate_operation(
|
||||
app.state.memory._operation_validator.validate_bank_write(ctx)
|
||||
)
|
||||
@@ -6322,9 +6804,11 @@ def _register_routes(app: FastAPI):
|
||||
# Authenticate and set schema context for multi-tenant DB queries
|
||||
await app.state.memory._authenticate_tenant(request_context)
|
||||
if app.state.memory._operation_validator:
|
||||
from hindsight_api.extensions import BankWriteContext
|
||||
from hindsight_api.extensions import BankWriteContext, BankWriteOperation
|
||||
|
||||
ctx = BankWriteContext(bank_id=bank_id, operation="reset_bank_config", request_context=request_context)
|
||||
ctx = BankWriteContext(
|
||||
bank_id=bank_id, operation=BankWriteOperation.RESET_BANK_CONFIG, request_context=request_context
|
||||
)
|
||||
await app.state.memory._validate_operation(
|
||||
app.state.memory._operation_validator.validate_bank_write(ctx)
|
||||
)
|
||||
@@ -6695,7 +7179,7 @@ def _register_routes(app: FastAPI):
|
||||
bank_id: str,
|
||||
request: RetainRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
_precheck: None = Depends(precheck_for("retain")),
|
||||
_precheck: None = Depends(precheck_for(PrecheckOperation.RETAIN)),
|
||||
):
|
||||
"""Retain memories with optional async processing."""
|
||||
metrics = get_metrics_collector()
|
||||
@@ -6878,7 +7362,7 @@ def _register_routes(app: FastAPI):
|
||||
files: list[UploadFile] = File(..., description="Files to upload and convert"),
|
||||
request: str = Form(..., description="JSON string with FileRetainRequest model"),
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
_precheck: None = Depends(precheck_for("files_retain")),
|
||||
_precheck: None = Depends(precheck_for(PrecheckOperation.FILES_RETAIN)),
|
||||
):
|
||||
"""Upload and convert files to memories."""
|
||||
from hindsight_api.config import get_config
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
"""Open Knowledge Format (OKF) projection for knowledge pages.
|
||||
|
||||
Knowledge pages are a *read-only* OKF view over the existing mental models: each
|
||||
mental model is projected into an OKF document — a markdown body with YAML
|
||||
frontmatter (``type`` required; ``title``/``description``/``tags``/``timestamp``
|
||||
optional) — and pages are linked into a constellation graph via shared tags.
|
||||
|
||||
See the Open Knowledge Format spec:
|
||||
https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf
|
||||
|
||||
This module is intentionally pure: every function transforms the mental-model
|
||||
dicts returned by ``MemoryEngine.list_mental_models`` / ``get_mental_model`` and
|
||||
never touches the database. That keeps the OKF contract unit-testable without a
|
||||
DB or LLM and lets the HTTP layer stay a thin wrapper.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
# OKF requires exactly one frontmatter field — ``type``. We default to this when
|
||||
# a page does not declare one via a ``type:<x>`` tag.
|
||||
DEFAULT_PAGE_TYPE = "knowledge-page"
|
||||
|
||||
# A page declares its OKF ``type`` through a tag of the form ``type:runbook``.
|
||||
# This keeps the projection schema-free (no new mental_models column): the type
|
||||
# is lifted from the existing tags array.
|
||||
TYPE_TAG_PREFIX = "type:"
|
||||
|
||||
INDEX_FILENAME = "index.md"
|
||||
|
||||
# Deterministic, colour-blind-friendly palette. Type → colour is stable across
|
||||
# requests so the constellation keeps the same colours between reloads.
|
||||
_PALETTE = (
|
||||
"#0074d9", # blue
|
||||
"#2ecc40", # green
|
||||
"#b10dc9", # purple
|
||||
"#ff851b", # orange
|
||||
"#39cccc", # teal
|
||||
"#f012be", # magenta
|
||||
"#3d9970", # olive
|
||||
"#ff4136", # red
|
||||
)
|
||||
|
||||
_EDGE_COLOR = "#9aa5b1"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PageType:
|
||||
"""A page's OKF ``type`` and the tags that remain after the type tag is split off."""
|
||||
|
||||
type: str
|
||||
display_tags: list[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KnowledgeGraph:
|
||||
"""Cytoscape-style node/edge graph of knowledge pages linked by shared tags."""
|
||||
|
||||
nodes: list[dict[str, Any]] = field(default_factory=list)
|
||||
edges: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
def _color_for(key: str) -> str:
|
||||
"""Stable colour for a string key (FNV-ish hash into the fixed palette)."""
|
||||
h = 0
|
||||
for ch in key:
|
||||
h = (h * 31 + ord(ch)) & 0xFFFFFFFF
|
||||
return _PALETTE[h % len(_PALETTE)]
|
||||
|
||||
|
||||
def _scalar(value: Any) -> str:
|
||||
"""Emit a YAML-safe double-quoted scalar.
|
||||
|
||||
We always double-quote so arbitrary page names / source queries can't be
|
||||
misread as YAML special forms (``true``, ``2026-01-01``, ``- x``, etc.).
|
||||
"""
|
||||
text = str(value)
|
||||
escaped = text.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "")
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def page_type(tags: list[str] | None) -> PageType:
|
||||
"""Split an OKF ``type`` out of the tag list.
|
||||
|
||||
The first ``type:<x>`` tag wins; all ``type:`` tags are removed from the
|
||||
returned ``display_tags`` so they don't pollute the constellation's
|
||||
shared-tag edges. Falls back to :data:`DEFAULT_PAGE_TYPE`.
|
||||
"""
|
||||
resolved = DEFAULT_PAGE_TYPE
|
||||
display: list[str] = []
|
||||
for tag in tags or []:
|
||||
if tag.startswith(TYPE_TAG_PREFIX):
|
||||
suffix = tag[len(TYPE_TAG_PREFIX) :].strip()
|
||||
if suffix and resolved == DEFAULT_PAGE_TYPE:
|
||||
resolved = suffix
|
||||
continue
|
||||
display.append(tag)
|
||||
return PageType(type=resolved, display_tags=display)
|
||||
|
||||
|
||||
def _timestamp(mm: dict[str, Any]) -> str | None:
|
||||
return mm.get("last_refreshed_at") or mm.get("created_at")
|
||||
|
||||
|
||||
def frontmatter(mm: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Build the ordered OKF frontmatter mapping for a mental model.
|
||||
|
||||
``None``/empty values are dropped by :func:`render_frontmatter`.
|
||||
"""
|
||||
pt = page_type(mm.get("tags"))
|
||||
return {
|
||||
"id": mm.get("id"),
|
||||
"type": pt.type,
|
||||
"title": mm.get("name"),
|
||||
"description": mm.get("source_query"),
|
||||
"tags": pt.display_tags,
|
||||
"timestamp": _timestamp(mm),
|
||||
}
|
||||
|
||||
|
||||
def render_frontmatter(fm: dict[str, Any]) -> str:
|
||||
"""Render a frontmatter mapping into a ``---`` fenced YAML block."""
|
||||
lines = ["---"]
|
||||
for key, value in fm.items():
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, list):
|
||||
if not value:
|
||||
continue
|
||||
lines.append(f"{key}:")
|
||||
lines.extend(f" - {_scalar(item)}" for item in value)
|
||||
else:
|
||||
lines.append(f"{key}: {_scalar(value)}")
|
||||
lines.append("---")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_document(mm: dict[str, Any]) -> str:
|
||||
"""Render a full OKF document: frontmatter block + markdown body."""
|
||||
body = (mm.get("content") or "").strip()
|
||||
return f"{render_frontmatter(frontmatter(mm))}\n\n{body}\n" if body else f"{render_frontmatter(frontmatter(mm))}\n"
|
||||
|
||||
|
||||
def page_filename(page_id: str) -> str:
|
||||
"""OKF bundle filename for a page id."""
|
||||
return f"{page_id}.md"
|
||||
|
||||
|
||||
def log_filename(page_id: str) -> str:
|
||||
"""OKF reserved per-page history filename."""
|
||||
return f"{page_id}.log.md"
|
||||
|
||||
|
||||
def render_index(nodes: list[dict[str, Any]]) -> str:
|
||||
"""Render the reserved ``index.md`` — nested OKF navigation over the tree.
|
||||
|
||||
``nodes`` is the flat folder/page list (each with ``id``, ``kind``, ``name``,
|
||||
``parent_id``); folders nest their children, pages link to their ``.md``.
|
||||
"""
|
||||
fm = render_frontmatter({"type": "index", "title": "Knowledge base"})
|
||||
lines = [fm, "", "# Knowledge base", ""]
|
||||
|
||||
children: dict[Any, list[dict[str, Any]]] = {}
|
||||
for node in nodes:
|
||||
children.setdefault(node.get("parent_id"), []).append(node)
|
||||
|
||||
def walk(parent: Any, depth: int) -> None:
|
||||
ordered = sorted(children.get(parent, []), key=lambda n: (n.get("sort_order", 0), n.get("name") or ""))
|
||||
for node in ordered:
|
||||
indent = " " * depth
|
||||
if node.get("kind") == "folder":
|
||||
lines.append(f"{indent}- **{node['name']}/**")
|
||||
walk(node["id"], depth + 1)
|
||||
else:
|
||||
description = node.get("source_query") or node.get("description")
|
||||
link = f"{indent}- [{node['name']}](./{page_filename(node['id'])})"
|
||||
lines.append(f"{link} — {description}" if description else link)
|
||||
|
||||
walk(None, 0)
|
||||
if len(lines) == 4:
|
||||
lines.append("_No knowledge pages yet._")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_log(mm: dict[str, Any], history: list[dict[str, Any]]) -> str:
|
||||
"""Render the reserved per-page ``log.md`` from refresh history.
|
||||
|
||||
Each history entry is ``{previous_content, previous_reflect_response,
|
||||
changed_at}`` (newest first), capturing the content *before* a refresh.
|
||||
"""
|
||||
name = mm.get("name") or mm.get("id")
|
||||
fm = render_frontmatter({"type": "log", "title": f"{name} — history"})
|
||||
lines = [fm, "", f"# {name} — history", ""]
|
||||
if not history:
|
||||
lines.append("_No refresh history._")
|
||||
return "\n".join(lines) + "\n"
|
||||
for entry in history:
|
||||
changed_at = entry.get("changed_at") or "unknown"
|
||||
previous = (entry.get("previous_content") or "").strip()
|
||||
lines.append(f"## {changed_at}")
|
||||
lines.append("")
|
||||
lines.append(previous if previous else "_(empty)_")
|
||||
lines.append("")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def knowledge_graph(
|
||||
pages: list[dict[str, Any]],
|
||||
cluster_for: "Callable[[dict[str, Any]], str] | None" = None,
|
||||
) -> KnowledgeGraph:
|
||||
"""Derive the constellation graph: pages as nodes, shared tags as edges.
|
||||
|
||||
Two pages are linked when they share at least one (non-``type:``) tag; the
|
||||
edge weight is the number of shared tags. Each node's cluster (``type`` field
|
||||
+ colour) comes from ``cluster_for(page)`` — the knowledge base groups by
|
||||
parent folder; the default groups by OKF ``type``.
|
||||
"""
|
||||
nodes: list[dict[str, Any]] = []
|
||||
tag_sets: list[tuple[str, frozenset[str]]] = []
|
||||
for mm in pages:
|
||||
page_id = mm["id"]
|
||||
pt = page_type(mm.get("tags"))
|
||||
cluster = cluster_for(mm) if cluster_for else pt.type
|
||||
tag_sets.append((page_id, frozenset(pt.display_tags)))
|
||||
nodes.append(
|
||||
{
|
||||
"data": {
|
||||
"id": page_id,
|
||||
"label": mm.get("name") or page_id,
|
||||
"type": cluster,
|
||||
"tagCount": len(pt.display_tags),
|
||||
"color": _color_for(cluster),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
edges: list[dict[str, Any]] = []
|
||||
for i in range(len(tag_sets)):
|
||||
source_id, source_tags = tag_sets[i]
|
||||
if not source_tags:
|
||||
continue
|
||||
for j in range(i + 1, len(tag_sets)):
|
||||
target_id, target_tags = tag_sets[j]
|
||||
shared = source_tags & target_tags
|
||||
if not shared:
|
||||
continue
|
||||
edges.append(
|
||||
{
|
||||
"data": {
|
||||
"id": f"{source_id}--{target_id}",
|
||||
"source": source_id,
|
||||
"target": target_id,
|
||||
"sharedTags": sorted(shared),
|
||||
"weight": len(shared),
|
||||
"color": _EDGE_COLOR,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return KnowledgeGraph(nodes=nodes, edges=edges)
|
||||
@@ -148,6 +148,19 @@ ENV_LLM_DEFAULT_HEADERS = "HINDSIGHT_API_LLM_DEFAULT_HEADERS"
|
||||
ENV_LLM_STRICT_SCHEMA = "HINDSIGHT_API_LLM_STRICT_SCHEMA"
|
||||
ENV_LLM_SEND_BANK_AS_USER = "HINDSIGHT_API_LLM_SEND_BANK_AS_USER"
|
||||
|
||||
# Per-operation sampling temperature. Each internal LLM call uses a temperature
|
||||
# tuned for its task (deterministic extraction vs. creative reflection). These
|
||||
# expose those as overridable knobs. Resolution per operation:
|
||||
# per-operation env -> global env (ENV_LLM_TEMPERATURE) -> built-in default.
|
||||
# A value of "none"/"default"/"" (or "off") omits the temperature parameter
|
||||
# entirely, for models that reject explicit temperatures (e.g. Azure GPT-5.5,
|
||||
# which only accepts the default value) -- see issue #2459.
|
||||
ENV_LLM_TEMPERATURE = "HINDSIGHT_API_LLM_TEMPERATURE"
|
||||
ENV_LLM_TEMPERATURE_VERIFICATION = "HINDSIGHT_API_LLM_TEMPERATURE_VERIFICATION"
|
||||
ENV_LLM_TEMPERATURE_RETAIN = "HINDSIGHT_API_LLM_TEMPERATURE_RETAIN"
|
||||
ENV_LLM_TEMPERATURE_REFLECT = "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT"
|
||||
ENV_LLM_TEMPERATURE_CONSOLIDATION = "HINDSIGHT_API_LLM_TEMPERATURE_CONSOLIDATION"
|
||||
|
||||
# Multi-LLM strategy. Extra LLMs are configured by index alongside the unindexed
|
||||
# primary (e.g. HINDSIGHT_API_LLM_1_PROVIDER, HINDSIGHT_API_LLM_2_PROVIDER, ...),
|
||||
# and HINDSIGHT_API_LLM_STRATEGY (JSON) selects how to route across them — see
|
||||
@@ -166,6 +179,12 @@ ENV_CONSOLIDATION_LLM_STRATEGY = "HINDSIGHT_API_CONSOLIDATION_LLM_STRATEGY"
|
||||
# disambiguates from the embeddings/reranker LITELLM_* settings.
|
||||
ENV_LLM_LITELLMROUTER_CONFIG = "HINDSIGHT_API_LLM_LITELLMROUTER_CONFIG"
|
||||
|
||||
# Per-operation temperature defaults (preserve historical hardcoded values).
|
||||
DEFAULT_LLM_TEMPERATURE_VERIFICATION = 0.0 # connection check
|
||||
DEFAULT_LLM_TEMPERATURE_RETAIN = 0.1 # fact extraction
|
||||
DEFAULT_LLM_TEMPERATURE_REFLECT = 0.9 # reflect "thinking"
|
||||
DEFAULT_LLM_TEMPERATURE_CONSOLIDATION = 0.0 # mental-model delta / dedup
|
||||
|
||||
# Defaults for service tiers
|
||||
DEFAULT_LLM_GROQ_SERVICE_TIER = "auto" # "on_demand", "flex", or "auto"
|
||||
DEFAULT_LLM_OPENAI_SERVICE_TIER = None # None (default) or "flex" (50% cheaper)
|
||||
@@ -189,6 +208,46 @@ def parse_gemini_service_tier(value: str | None) -> str | None:
|
||||
return tier
|
||||
|
||||
|
||||
# Sentinel strings that, as a temperature value, mean "omit the temperature
|
||||
# parameter entirely" rather than a numeric setting.
|
||||
_TEMPERATURE_OMIT_VALUES = frozenset({"", "none", "default", "off", "unset"})
|
||||
|
||||
|
||||
def _parse_temperature(raw: str) -> float | None:
|
||||
"""Parse a raw temperature env value into a float, or None to omit it.
|
||||
|
||||
Returns None for the omit sentinels (so the temperature parameter is dropped
|
||||
from the LLM call); otherwise parses a float and validates the 0.0-2.0 range.
|
||||
"""
|
||||
if raw.strip().lower() in _TEMPERATURE_OMIT_VALUES:
|
||||
return None
|
||||
try:
|
||||
value = float(raw)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
f"Invalid LLM temperature {raw!r}: must be a number in [0.0, 2.0] "
|
||||
f"or one of {sorted(_TEMPERATURE_OMIT_VALUES)} to omit it."
|
||||
) from e
|
||||
if not 0.0 <= value <= 2.0:
|
||||
raise ValueError(f"Invalid LLM temperature {value}: must be in [0.0, 2.0].")
|
||||
return value
|
||||
|
||||
|
||||
def _resolve_operation_temperature(operation_env: str, default: float) -> float | None:
|
||||
"""Resolve a per-operation temperature: per-op env -> global env -> default.
|
||||
|
||||
The omit sentinels resolve to None at any layer, so a single
|
||||
``HINDSIGHT_API_LLM_TEMPERATURE=none`` drops temperature from every operation
|
||||
that has no explicit per-operation override.
|
||||
"""
|
||||
raw = os.getenv(operation_env)
|
||||
if raw is None:
|
||||
raw = os.getenv(ENV_LLM_TEMPERATURE)
|
||||
if raw is None:
|
||||
return default
|
||||
return _parse_temperature(raw)
|
||||
|
||||
|
||||
# Per-operation LLM configuration (optional, falls back to global LLM config)
|
||||
ENV_RETAIN_LLM_PROVIDER = "HINDSIGHT_API_RETAIN_LLM_PROVIDER"
|
||||
ENV_RETAIN_LLM_API_KEY = "HINDSIGHT_API_RETAIN_LLM_API_KEY"
|
||||
@@ -514,7 +573,6 @@ ENV_LLAMACPP_EXTRA_ARGS = "HINDSIGHT_API_LLAMACPP_EXTRA_ARGS"
|
||||
|
||||
# Optimization flags
|
||||
ENV_SKIP_LLM_VERIFICATION = "HINDSIGHT_API_SKIP_LLM_VERIFICATION"
|
||||
ENV_LAZY_RERANKER = "HINDSIGHT_API_LAZY_RERANKER"
|
||||
|
||||
# Database migrations
|
||||
ENV_RUN_MIGRATIONS_ON_STARTUP = "HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP"
|
||||
@@ -1520,6 +1578,14 @@ class HindsightConfig:
|
||||
# overrides a `user` the caller already set.
|
||||
llm_send_bank_as_user: bool
|
||||
|
||||
# Per-operation sampling temperature. None means the temperature parameter is
|
||||
# omitted from the call (for models that reject explicit temperatures). See
|
||||
# ENV_LLM_TEMPERATURE and _resolve_operation_temperature.
|
||||
llm_temperature_verification: float | None
|
||||
llm_temperature_retain: float | None
|
||||
llm_temperature_reflect: float | None
|
||||
llm_temperature_consolidation: float | None
|
||||
|
||||
# LiteLLM Router chain (provider-specific; consumed by the "litellmrouter" provider).
|
||||
# List of deployment dicts evaluated in order with fallback on transient errors.
|
||||
# Each entry: {"provider": str, "model": str, "api_key": str | None, "base_url": str | None}.
|
||||
@@ -1819,7 +1885,6 @@ class HindsightConfig:
|
||||
|
||||
# Optimization flags
|
||||
skip_llm_verification: bool
|
||||
lazy_reranker: bool
|
||||
|
||||
# Database migrations
|
||||
run_migrations_on_startup: bool
|
||||
@@ -2254,6 +2319,18 @@ class HindsightConfig:
|
||||
llm_strict_schema=os.getenv(ENV_LLM_STRICT_SCHEMA, str(DEFAULT_LLM_STRICT_SCHEMA)).lower() in ("true", "1"),
|
||||
llm_send_bank_as_user=os.getenv(ENV_LLM_SEND_BANK_AS_USER, str(DEFAULT_LLM_SEND_BANK_AS_USER)).lower()
|
||||
in ("true", "1"),
|
||||
llm_temperature_verification=_resolve_operation_temperature(
|
||||
ENV_LLM_TEMPERATURE_VERIFICATION, DEFAULT_LLM_TEMPERATURE_VERIFICATION
|
||||
),
|
||||
llm_temperature_retain=_resolve_operation_temperature(
|
||||
ENV_LLM_TEMPERATURE_RETAIN, DEFAULT_LLM_TEMPERATURE_RETAIN
|
||||
),
|
||||
llm_temperature_reflect=_resolve_operation_temperature(
|
||||
ENV_LLM_TEMPERATURE_REFLECT, DEFAULT_LLM_TEMPERATURE_REFLECT
|
||||
),
|
||||
llm_temperature_consolidation=_resolve_operation_temperature(
|
||||
ENV_LLM_TEMPERATURE_CONSOLIDATION, DEFAULT_LLM_TEMPERATURE_CONSOLIDATION
|
||||
),
|
||||
llm_litellmrouter_config=_parse_llm_router_config(ENV_LLM_LITELLMROUTER_CONFIG),
|
||||
# Vertex AI
|
||||
llm_vertexai_project_id=os.getenv(ENV_LLM_VERTEXAI_PROJECT_ID) or DEFAULT_LLM_VERTEXAI_PROJECT_ID,
|
||||
@@ -2651,7 +2728,6 @@ class HindsightConfig:
|
||||
),
|
||||
# Optimization flags
|
||||
skip_llm_verification=os.getenv(ENV_SKIP_LLM_VERIFICATION, "false").lower() == "true",
|
||||
lazy_reranker=os.getenv(ENV_LAZY_RERANKER, "false").lower() == "true",
|
||||
# Retain settings
|
||||
retain_max_completion_tokens=int(
|
||||
os.getenv(ENV_RETAIN_MAX_COMPLETION_TOKENS, str(DEFAULT_RETAIN_MAX_COMPLETION_TOKENS))
|
||||
|
||||
@@ -13,9 +13,18 @@ in-flight task so that N concurrent callers produce one query rather than N.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Awaitable, Callable
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
||||
|
||||
from .db_utils import acquire_with_retry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .db.base import DatabaseBackend
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BankStatsCache:
|
||||
@@ -66,17 +75,28 @@ class BankStatsCache:
|
||||
schema: str,
|
||||
bank_id: str,
|
||||
loader: Callable[[], Awaitable[dict[str, Any]]],
|
||||
*,
|
||||
force_refresh: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Return cached stats for `(schema, bank_id)` or call `loader()`.
|
||||
|
||||
Concurrent misses on the same key are coalesced onto a single
|
||||
in-flight loader.
|
||||
in-flight loader. When ``force_refresh`` is set the cached value is
|
||||
ignored: the loader runs and its result replaces the cached entry.
|
||||
"""
|
||||
if not self.enabled:
|
||||
return await loader()
|
||||
|
||||
key = (schema, bank_id)
|
||||
|
||||
if force_refresh:
|
||||
value = await loader()
|
||||
async with self._lock:
|
||||
self._store_unlocked(key, value)
|
||||
# Supersede any loader that was in flight for this key.
|
||||
self._in_flight.pop(key, None)
|
||||
return value
|
||||
|
||||
async with self._lock:
|
||||
cached = self._get_fresh_unlocked(key)
|
||||
if cached is not None:
|
||||
@@ -132,3 +152,103 @@ class BankStatsCache:
|
||||
async with self._lock:
|
||||
self._entries.clear()
|
||||
self._in_flight.clear()
|
||||
|
||||
|
||||
class DistributedBankStatsCache:
|
||||
"""Table-backed (cross-process) TTL cache for `get_bank_stats`.
|
||||
|
||||
Same ``get_or_load`` / ``invalidate`` / ``clear`` contract as
|
||||
:class:`BankStatsCache`, but the store is the per-schema ``bank_stats_cache``
|
||||
table instead of a per-process dict — so one worker's computation is shared
|
||||
with every other worker, and no caller recomputes while a fresh row exists.
|
||||
|
||||
On a hit, a call is a single primary-key ``SELECT`` (sub-millisecond); only a
|
||||
miss runs the (expensive) ``loader`` and writes the row back. Concurrent
|
||||
misses are *not* coalesced across processes (that would need a lock): they
|
||||
each compute and ``UPSERT``, last write wins — all results are correct, at the
|
||||
cost of a brief redundant compute at expiry.
|
||||
|
||||
Every DB touch is best-effort: if the cache table is unreachable or missing
|
||||
(e.g. a schema mid-migration), the call degrades to computing without caching
|
||||
rather than failing ``get_bank_stats``. PostgreSQL only — the engine keeps the
|
||||
in-process :class:`BankStatsCache` for Oracle.
|
||||
"""
|
||||
|
||||
def __init__(self, *, backend: "DatabaseBackend", ttl_seconds: float) -> None:
|
||||
self._backend = backend
|
||||
self._ttl = float(ttl_seconds)
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self._ttl > 0
|
||||
|
||||
@staticmethod
|
||||
def _qualified(schema: str) -> str:
|
||||
return f'"{schema}".bank_stats_cache' if schema else "bank_stats_cache"
|
||||
|
||||
async def get_or_load(
|
||||
self,
|
||||
schema: str,
|
||||
bank_id: str,
|
||||
loader: Callable[[], Awaitable[dict[str, Any]]],
|
||||
*,
|
||||
force_refresh: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
if not self.enabled:
|
||||
return await loader()
|
||||
|
||||
table = self._qualified(schema)
|
||||
|
||||
# 1. Fresh row? Single PK lookup; ``payload::text`` sidesteps any
|
||||
# jsonb->object codec so we always decode the same way. Skipped when
|
||||
# the caller forces a refresh — then we recompute and overwrite below.
|
||||
if not force_refresh:
|
||||
try:
|
||||
async with acquire_with_retry(self._backend) as conn:
|
||||
row = await conn.fetchrow(
|
||||
f"SELECT payload::text AS payload FROM {table} "
|
||||
f"WHERE bank_id = $1 AND computed_at > now() - make_interval(secs => $2::double precision)",
|
||||
bank_id,
|
||||
self._ttl,
|
||||
)
|
||||
if row is not None:
|
||||
return json.loads(row["payload"])
|
||||
except Exception as exc: # noqa: BLE001 — cache read must never break the endpoint
|
||||
logger.debug("bank_stats_cache read failed for %s.%s (%s); computing uncached", schema, bank_id, exc)
|
||||
return await loader()
|
||||
|
||||
# 2. Miss — compute, then write the row back (best-effort).
|
||||
value = await loader()
|
||||
try:
|
||||
async with acquire_with_retry(self._backend) as conn:
|
||||
await conn.execute(
|
||||
f"INSERT INTO {table} (bank_id, payload, computed_at) VALUES ($1, $2::jsonb, now()) "
|
||||
f"ON CONFLICT (bank_id) DO UPDATE SET payload = EXCLUDED.payload, computed_at = now()",
|
||||
bank_id,
|
||||
json.dumps(value),
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 — a failed write just means no caching this round
|
||||
logger.warning("bank_stats_cache write failed for %s.%s (%s)", schema, bank_id, exc)
|
||||
return value
|
||||
|
||||
async def invalidate(self, schema: str, bank_id: str) -> None:
|
||||
"""Drop the cached row so the next read recomputes."""
|
||||
if not self.enabled:
|
||||
return
|
||||
try:
|
||||
async with acquire_with_retry(self._backend) as conn:
|
||||
await conn.execute(f"DELETE FROM {self._qualified(schema)} WHERE bank_id = $1", bank_id)
|
||||
except Exception as exc: # noqa: BLE001 — invalidation must never break the write path
|
||||
logger.debug("bank_stats_cache invalidate failed for %s.%s (%s)", schema, bank_id, exc)
|
||||
|
||||
async def clear(self) -> None:
|
||||
"""Drop all cached rows in the current schema (best-effort)."""
|
||||
if not self.enabled:
|
||||
return
|
||||
from .memory_engine import get_current_schema
|
||||
|
||||
try:
|
||||
async with acquire_with_retry(self._backend) as conn:
|
||||
await conn.execute(f"DELETE FROM {self._qualified(get_current_schema())}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug("bank_stats_cache clear failed (%s)", exc)
|
||||
|
||||
@@ -98,7 +98,7 @@ _DEDUP_TOP_K = 5
|
||||
class _DedupDecision(BaseModel):
|
||||
"""Focused 1-by-1 verdict for whether a new observation duplicates an existing one."""
|
||||
|
||||
action: Literal["merge", "keep"]
|
||||
action: Literal["merge", "keep"] = "keep"
|
||||
text: str = "" # the synthesized merged observation (when action == "merge")
|
||||
reason: str = ""
|
||||
|
||||
@@ -224,13 +224,18 @@ async def _dedup_reconcile_create(
|
||||
# Fold the new source facts into the twin and persist the merged text. We keep the twin's
|
||||
# existing embedding: the merged text is >= threshold similar, so the stored vector stays
|
||||
# representative and we avoid a re-embed + a dialect-specific vector UPDATE.
|
||||
search_vector_clause = (
|
||||
f",\n search_vector = to_tsvector('{config.text_search_extension_native_language}'::regconfig, COALESCE($1, ''))"
|
||||
if config.text_search_extension == "native"
|
||||
else ""
|
||||
)
|
||||
await conn.execute(
|
||||
f"""
|
||||
UPDATE {fq_table("memory_units")}
|
||||
SET text = $1,
|
||||
source_memory_ids = (SELECT array_agg(DISTINCT e) FROM unnest(source_memory_ids || $2::uuid[]) e),
|
||||
proof_count = (SELECT count(DISTINCT e) FROM unnest(source_memory_ids || $2::uuid[]) e),
|
||||
updated_at = now()
|
||||
updated_at = now(){search_vector_clause}
|
||||
WHERE id = $3::uuid
|
||||
""",
|
||||
outcome.merged_text,
|
||||
@@ -279,6 +284,11 @@ async def _dedup_reconcile_update(
|
||||
# the create path) then delete the now-redundant updated row. The all_strict/any tag match
|
||||
# guarantees twin and updated share scope, so dropping the updated row's tags loses no
|
||||
# visibility. Temporal fields follow the surviving twin (minimal scope; matches create).
|
||||
search_vector_clause = (
|
||||
f",\n search_vector = to_tsvector('{config.text_search_extension_native_language}'::regconfig, COALESCE($1, ''))"
|
||||
if config.text_search_extension == "native"
|
||||
else ""
|
||||
)
|
||||
await conn.execute(
|
||||
f"""
|
||||
UPDATE {fq_table("memory_units")} t
|
||||
@@ -289,7 +299,7 @@ async def _dedup_reconcile_update(
|
||||
proof_count = (
|
||||
SELECT count(DISTINCT e) FROM unnest(t.source_memory_ids || u.source_memory_ids) e
|
||||
),
|
||||
updated_at = now()
|
||||
updated_at = now(){search_vector_clause}
|
||||
FROM {fq_table("memory_units")} u
|
||||
WHERE t.id = $2::uuid AND u.id = $3::uuid
|
||||
""",
|
||||
@@ -1845,6 +1855,12 @@ async def _execute_update_action(
|
||||
|
||||
config = get_config()
|
||||
|
||||
search_vector_clause = (
|
||||
f",\n search_vector = to_tsvector('{config.text_search_extension_native_language}'::regconfig, COALESCE($1, ''))"
|
||||
if config.text_search_extension == "native"
|
||||
else ""
|
||||
)
|
||||
|
||||
t0 = time.time()
|
||||
await conn.execute(
|
||||
f"""
|
||||
@@ -1857,7 +1873,7 @@ async def _execute_update_action(
|
||||
updated_at = now(),
|
||||
occurred_start = LEAST(occurred_start, COALESCE($6, occurred_start)),
|
||||
occurred_end = GREATEST(occurred_end, COALESCE($7, occurred_end)),
|
||||
mentioned_at = GREATEST(mentioned_at, COALESCE($8, mentioned_at))
|
||||
mentioned_at = GREATEST(mentioned_at, COALESCE($8, mentioned_at)){search_vector_clause}
|
||||
WHERE id = $5
|
||||
""",
|
||||
new_text,
|
||||
@@ -2333,16 +2349,20 @@ async def _create_observation_directly(
|
||||
tokenize($3, 'llmlingua2')::bm25_catalog.bm25vector)
|
||||
RETURNING id
|
||||
"""
|
||||
else: # native, pg_textsearch, pgroonga, or pg_search
|
||||
# pg_textsearch / pgroonga / pg_search: indexes operate on base text
|
||||
# columns directly, so the dummy search_vector column is left NULL.
|
||||
# Native: the migration p4q5r6s7t8u9 dropped the GENERATED expression on
|
||||
# search_vector to allow per-deployment language configuration; the
|
||||
# batch insert path in ops_postgresql.insert_facts_batch now populates
|
||||
# it via to_tsvector($lang, ...). This single-observation INSERT does
|
||||
# not, so observations under the native backend currently land with
|
||||
# NULL search_vector and are not BM25-searchable until reflected/
|
||||
# re-ingested. Tracking a separate fix for that gap.
|
||||
elif config.text_search_extension == "native":
|
||||
# Native: search_vector is populated with to_tsvector() using the
|
||||
# configured native language dictionary, matching the batch insert
|
||||
# path in ops_postgresql.insert_facts_batch.
|
||||
query = f"""
|
||||
INSERT INTO {fq_table("memory_units")} (
|
||||
id, bank_id, text, fact_type, embedding, proof_count, source_memory_ids,
|
||||
tags, event_date, occurred_start, occurred_end, mentioned_at, search_vector
|
||||
)
|
||||
VALUES ($1, $2, $3, 'observation', $4::vector, 1, $5, $6, $7, $8, $9, $10,
|
||||
to_tsvector('{config.text_search_extension_native_language}'::regconfig, COALESCE($3, '')))
|
||||
RETURNING id
|
||||
"""
|
||||
else: # pg_textsearch, pgroonga, pg_search: indexes operate on base text columns directly
|
||||
query = f"""
|
||||
INSERT INTO {fq_table("memory_units")} (
|
||||
id, bank_id, text, fact_type, embedding, proof_count, source_memory_ids,
|
||||
|
||||
@@ -449,6 +449,7 @@ class MemoryEngineInterface(ABC):
|
||||
bank_id: str,
|
||||
*,
|
||||
request_context: "RequestContext",
|
||||
force_refresh: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get statistics about memory nodes and links for a bank.
|
||||
@@ -456,6 +457,8 @@ class MemoryEngineInterface(ABC):
|
||||
Args:
|
||||
bank_id: The memory bank ID.
|
||||
request_context: Request context for authentication.
|
||||
force_refresh: Bypass the cached value and recompute (also refreshes
|
||||
the cache for subsequent callers).
|
||||
|
||||
Returns:
|
||||
Dict with node_counts, link_counts, link_counts_by_fact_type
|
||||
|
||||
@@ -253,6 +253,7 @@ def create_llm_provider(
|
||||
prompt_cache_enabled: bool = False,
|
||||
litellmrouter_config: dict[str, Any] | None = None,
|
||||
gemini_service_tier: str | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> Any: # Returns LLMInterface
|
||||
"""
|
||||
Factory function to create the appropriate LLM provider implementation.
|
||||
@@ -272,12 +273,20 @@ def create_llm_provider(
|
||||
VertexAI and LiteLLM providers (each merges them in its own parameter
|
||||
space). Keys must use each provider's native names (e.g. ``max_tokens``
|
||||
for OpenAI/Anthropic vs ``max_output_tokens`` for Gemini).
|
||||
default_headers: Custom headers passed as ``default_headers`` to provider SDK clients
|
||||
(used by operators routing through proxies / request-tracing middleware). Currently
|
||||
wired into the Anthropic provider; other providers may opt in as needed.
|
||||
default_headers: Custom headers passed to provider SDK clients (used by operators
|
||||
routing through proxies / request-tracing middleware). Wired into the Anthropic
|
||||
provider (SDK ``default_headers``) and the LiteLLM-backed providers — ``litellm``,
|
||||
``litellmrouter`` and ``bedrock`` — as the LiteLLM ``extra_headers`` completion
|
||||
kwarg; other providers may opt in as needed.
|
||||
vertexai_project_id: Vertex AI project ID (for VertexAI provider).
|
||||
vertexai_region: Vertex AI region (for VertexAI provider).
|
||||
vertexai_credentials: Vertex AI credentials object (for VertexAI provider).
|
||||
timeout: Per-request LLM timeout in seconds (resolved by the caller from the
|
||||
per-operation/global config). Threaded into the providers that honour a
|
||||
configurable request timeout (LiteLLM, LiteLLM Router, OpenAI-compatible,
|
||||
Nous). ``None`` lets each provider fall back to its own default
|
||||
(``HINDSIGHT_API_LLM_TIMEOUT`` / ``DEFAULT_LLM_TIMEOUT`` for those four;
|
||||
Anthropic and Gemini keep their provider-specific defaults).
|
||||
|
||||
Returns:
|
||||
LLMInterface implementation for the specified provider.
|
||||
@@ -375,6 +384,8 @@ def create_llm_provider(
|
||||
model=model,
|
||||
reasoning_effort=reasoning_effort,
|
||||
extra_body=extra_body,
|
||||
default_headers=default_headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
elif provider_lower == "litellmrouter":
|
||||
@@ -393,6 +404,8 @@ def create_llm_provider(
|
||||
config=litellmrouter_config,
|
||||
reasoning_effort=reasoning_effort,
|
||||
extra_body=extra_body,
|
||||
default_headers=default_headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
elif provider_lower == "bedrock":
|
||||
@@ -405,7 +418,9 @@ def create_llm_provider(
|
||||
model=bedrock_model,
|
||||
reasoning_effort=reasoning_effort,
|
||||
extra_body=extra_body,
|
||||
default_headers=default_headers,
|
||||
bedrock_service_tier=bedrock_service_tier,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
elif provider_lower == "llamacpp":
|
||||
@@ -452,6 +467,7 @@ def create_llm_provider(
|
||||
model=model,
|
||||
reasoning_effort=reasoning_effort,
|
||||
extra_body=extra_body,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
elif provider_lower in (
|
||||
@@ -478,6 +494,7 @@ def create_llm_provider(
|
||||
groq_service_tier=groq_service_tier,
|
||||
openai_service_tier=openai_service_tier,
|
||||
extra_body=extra_body,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -510,6 +527,10 @@ class LLMProvider:
|
||||
vertexai_project_id: str | None = None,
|
||||
vertexai_region: str | None = None,
|
||||
vertexai_service_account_key: str | None = None,
|
||||
timeout: float | None = None,
|
||||
max_retries: int | None = None,
|
||||
initial_backoff: float | None = None,
|
||||
max_backoff: float | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize LLM provider.
|
||||
@@ -538,6 +559,17 @@ class LLMProvider:
|
||||
``"us-central1"`` when ``None``).
|
||||
vertexai_service_account_key: Path to a Vertex AI service-account key file for
|
||||
``provider="vertexai"`` (uses ADC when ``None``).
|
||||
timeout: Per-request LLM timeout in seconds. Resolved by the caller from the
|
||||
per-operation/global config (``retain_llm_timeout`` falling back to
|
||||
``llm_timeout``, etc.). ``None`` lets each provider apply its own default.
|
||||
max_retries: Default retry-attempt budget for ``call`` / ``call_with_tools``
|
||||
when the per-call argument is omitted. Resolved by the caller from the
|
||||
per-operation/global config (``reflect_llm_max_retries`` falling back to
|
||||
``llm_max_retries``, etc.). ``None`` keeps each method's own fallback.
|
||||
initial_backoff: Default initial retry backoff (seconds), same resolution as
|
||||
``max_retries``. ``None`` keeps each method's own fallback.
|
||||
max_backoff: Default maximum retry backoff (seconds), same resolution as
|
||||
``max_retries``. ``None`` keeps each method's own fallback.
|
||||
|
||||
This constructor uses every argument as passed and does not read global
|
||||
``HindsightConfig``: resolving the server-level default for a ``None`` argument is the
|
||||
@@ -551,6 +583,15 @@ class LLMProvider:
|
||||
self.base_url = base_url
|
||||
self.model = model
|
||||
self.reasoning_effort = reasoning_effort
|
||||
# Per-request timeout (seconds). Used verbatim — the caller resolves the
|
||||
# per-operation/global fallback. ``None`` defers to the provider default.
|
||||
self.timeout = timeout
|
||||
# Default retry policy for call()/call_with_tools(). The caller resolves the
|
||||
# per-operation/global fallback; ``None`` keeps each method's own fallback so
|
||||
# providers built without a resolved config (from_env, tests) are unchanged.
|
||||
self.max_retries = max_retries
|
||||
self.initial_backoff = initial_backoff
|
||||
self.max_backoff = max_backoff
|
||||
self.litellmrouter_config = litellmrouter_config
|
||||
# Service tiers from hierarchical config (not env vars)
|
||||
self.groq_service_tier = groq_service_tier
|
||||
@@ -701,6 +742,7 @@ class LLMProvider:
|
||||
gemini_safety_settings=self.gemini_safety_settings,
|
||||
prompt_cache_enabled=self.prompt_cache_enabled,
|
||||
litellmrouter_config=router_config,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
|
||||
# Backward compatibility: Keep mock provider properties
|
||||
@@ -757,9 +799,9 @@ class LLMProvider:
|
||||
max_completion_tokens: int | None = None,
|
||||
temperature: float | None = None,
|
||||
scope: str = "memory",
|
||||
max_retries: int = 10,
|
||||
initial_backoff: float = 1.0,
|
||||
max_backoff: float = 60.0,
|
||||
max_retries: int | None = None,
|
||||
initial_backoff: float | None = None,
|
||||
max_backoff: float | None = None,
|
||||
skip_validation: bool = False,
|
||||
strict_schema: bool = False,
|
||||
return_usage: bool = False,
|
||||
@@ -774,9 +816,12 @@ class LLMProvider:
|
||||
max_completion_tokens: Maximum tokens in response.
|
||||
temperature: Sampling temperature (0.0-2.0).
|
||||
scope: Scope identifier for tracking.
|
||||
max_retries: Maximum retry attempts.
|
||||
initial_backoff: Initial backoff time in seconds.
|
||||
max_backoff: Maximum backoff time in seconds.
|
||||
max_retries: Maximum retry attempts. ``None`` uses the provider's configured
|
||||
default (per-operation/global ``llm_max_retries``), else 10.
|
||||
initial_backoff: Initial backoff time in seconds. ``None`` uses the provider's
|
||||
configured default (``llm_initial_backoff``), else 1.0.
|
||||
max_backoff: Maximum backoff time in seconds. ``None`` uses the provider's
|
||||
configured default (``llm_max_backoff``), else 60.0.
|
||||
skip_validation: Return raw JSON without Pydantic validation.
|
||||
strict_schema: Per-call override requesting grammar-enforced (json_schema strict)
|
||||
structured output instead of the soft json_object path. The server-level
|
||||
@@ -801,6 +846,20 @@ class LLMProvider:
|
||||
structured = "+structured" if response_format is not None else ""
|
||||
set_stage(f"llm.{self.provider}.{scope}{structured}")
|
||||
|
||||
# Resolve the retry policy: explicit per-call arg wins, else the provider's
|
||||
# configured per-operation/global default, else this method's own fallback.
|
||||
max_retries = (
|
||||
max_retries if max_retries is not None else (self.max_retries if self.max_retries is not None else 10)
|
||||
)
|
||||
initial_backoff = (
|
||||
initial_backoff
|
||||
if initial_backoff is not None
|
||||
else (self.initial_backoff if self.initial_backoff is not None else 1.0)
|
||||
)
|
||||
max_backoff = (
|
||||
max_backoff if max_backoff is not None else (self.max_backoff if self.max_backoff is not None else 60.0)
|
||||
)
|
||||
|
||||
# Resolve strict-schema once, here, rather than in each provider: the
|
||||
# per-call argument OR the server-level HINDSIGHT_API_LLM_STRICT_SCHEMA
|
||||
# flag. Providers with a json_schema response_format (OpenAI-compatible,
|
||||
@@ -903,9 +962,9 @@ class LLMProvider:
|
||||
max_completion_tokens: int | None = None,
|
||||
temperature: float | None = None,
|
||||
scope: str = "tools",
|
||||
max_retries: int = 5,
|
||||
initial_backoff: float = 1.0,
|
||||
max_backoff: float = 30.0,
|
||||
max_retries: int | None = None,
|
||||
initial_backoff: float | None = None,
|
||||
max_backoff: float | None = None,
|
||||
tool_choice: str | dict[str, Any] = "auto",
|
||||
cached_prefix: str | None = None,
|
||||
) -> "LLMToolCallResult":
|
||||
@@ -918,9 +977,12 @@ class LLMProvider:
|
||||
max_completion_tokens: Maximum tokens in response.
|
||||
temperature: Sampling temperature (0.0-2.0).
|
||||
scope: Scope identifier for tracking.
|
||||
max_retries: Maximum retry attempts.
|
||||
initial_backoff: Initial backoff time in seconds.
|
||||
max_backoff: Maximum backoff time in seconds.
|
||||
max_retries: Maximum retry attempts. ``None`` uses the provider's configured
|
||||
default (per-operation/global ``llm_max_retries``), else 5.
|
||||
initial_backoff: Initial backoff time in seconds. ``None`` uses the provider's
|
||||
configured default (``llm_initial_backoff``), else 1.0.
|
||||
max_backoff: Maximum backoff time in seconds. ``None`` uses the provider's
|
||||
configured default (``llm_max_backoff``), else 30.0.
|
||||
tool_choice: How to choose tools - "auto", "none", "required", or {"type": "function", "function": {"name": "..."}}
|
||||
|
||||
Returns:
|
||||
@@ -930,6 +992,20 @@ class LLMProvider:
|
||||
|
||||
set_stage(f"llm.{self.provider}.{scope}+tools")
|
||||
|
||||
# Resolve the retry policy: explicit per-call arg wins, else the provider's
|
||||
# configured per-operation/global default, else this method's own fallback.
|
||||
max_retries = (
|
||||
max_retries if max_retries is not None else (self.max_retries if self.max_retries is not None else 5)
|
||||
)
|
||||
initial_backoff = (
|
||||
initial_backoff
|
||||
if initial_backoff is not None
|
||||
else (self.initial_backoff if self.initial_backoff is not None else 1.0)
|
||||
)
|
||||
max_backoff = (
|
||||
max_backoff if max_backoff is not None else (self.max_backoff if self.max_backoff is not None else 30.0)
|
||||
)
|
||||
|
||||
# Failures forwarded to the GenAI recorder; successes recorded by providers.
|
||||
from ..tracing import get_span_recorder
|
||||
from .llm_trace import (
|
||||
@@ -1168,9 +1244,12 @@ class LLMProvider:
|
||||
# does so without building the full HindsightConfig, keeping from_env() a
|
||||
# lightweight env-only loader (see test_llm_provider_from_env_keeps_lightweight_loader).
|
||||
from ..config import (
|
||||
DEFAULT_LLM_GROQ_SERVICE_TIER,
|
||||
DEFAULT_LLM_OPENAI_SERVICE_TIER,
|
||||
DEFAULT_LLM_PROMPT_CACHE_ENABLED,
|
||||
DEFAULT_LLM_PROVIDER,
|
||||
DEFAULT_LLM_REASONING_EFFORT,
|
||||
DEFAULT_LLM_TIMEOUT,
|
||||
ENV_LLM_API_KEY,
|
||||
ENV_LLM_BASE_URL,
|
||||
ENV_LLM_BEDROCK_SERVICE_TIER,
|
||||
@@ -1178,11 +1257,14 @@ class LLMProvider:
|
||||
ENV_LLM_EXTRA_BODY,
|
||||
ENV_LLM_GEMINI_SAFETY_SETTINGS,
|
||||
ENV_LLM_GEMINI_SERVICE_TIER,
|
||||
ENV_LLM_GROQ_SERVICE_TIER,
|
||||
ENV_LLM_LITELLMROUTER_CONFIG,
|
||||
ENV_LLM_MODEL,
|
||||
ENV_LLM_OPENAI_SERVICE_TIER,
|
||||
ENV_LLM_PROMPT_CACHE_ENABLED,
|
||||
ENV_LLM_PROVIDER,
|
||||
ENV_LLM_REASONING_EFFORT,
|
||||
ENV_LLM_TIMEOUT,
|
||||
ENV_LLM_VERTEXAI_PROJECT_ID,
|
||||
ENV_LLM_VERTEXAI_REGION,
|
||||
ENV_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY,
|
||||
@@ -1222,6 +1304,8 @@ class LLMProvider:
|
||||
reasoning_effort=os.getenv(ENV_LLM_REASONING_EFFORT, DEFAULT_LLM_REASONING_EFFORT),
|
||||
extra_body=extra_body,
|
||||
default_headers=default_headers,
|
||||
groq_service_tier=os.getenv(ENV_LLM_GROQ_SERVICE_TIER, DEFAULT_LLM_GROQ_SERVICE_TIER),
|
||||
openai_service_tier=os.getenv(ENV_LLM_OPENAI_SERVICE_TIER, DEFAULT_LLM_OPENAI_SERVICE_TIER),
|
||||
bedrock_service_tier=os.getenv(ENV_LLM_BEDROCK_SERVICE_TIER) or None,
|
||||
gemini_service_tier=(
|
||||
parse_gemini_service_tier(os.getenv(ENV_LLM_GEMINI_SERVICE_TIER))
|
||||
@@ -1234,6 +1318,7 @@ class LLMProvider:
|
||||
vertexai_project_id=os.getenv(ENV_LLM_VERTEXAI_PROJECT_ID) or None,
|
||||
vertexai_region=os.getenv(ENV_LLM_VERTEXAI_REGION) or None,
|
||||
vertexai_service_account_key=os.getenv(ENV_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY) or None,
|
||||
timeout=float(os.getenv(ENV_LLM_TIMEOUT, str(DEFAULT_LLM_TIMEOUT))),
|
||||
)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,13 +5,35 @@ import logging
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from hindsight_api.config import DEFAULT_FILE_PARSER_MARKITDOWN_OCR_PROMPT
|
||||
|
||||
from .base import FileParser
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from markitdown import StreamInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Extensions whose markitdown converters decode the raw bytes as text. markitdown
|
||||
# samples only the first chunk for charset detection, so a UTF-8 file with a long
|
||||
# ASCII-only prefix is mis-detected as ASCII; the JSON/ipynb converter then crashes
|
||||
# decoding the first multibyte byte. Passing an explicit UTF-8 hint when the bytes
|
||||
# are valid UTF-8 sidesteps the faulty detection without affecting other encodings.
|
||||
_TEXT_EXTENSIONS = {
|
||||
".json",
|
||||
".jsonl",
|
||||
".ipynb",
|
||||
".txt",
|
||||
".text",
|
||||
".md",
|
||||
".markdown",
|
||||
".csv",
|
||||
".html",
|
||||
".htm",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MarkitdownOcrOptions:
|
||||
@@ -134,8 +156,9 @@ class MarkitdownParser(FileParser):
|
||||
tmp_path = tmp.name
|
||||
|
||||
try:
|
||||
# Parse using markitdown
|
||||
result = self._markitdown.convert(tmp_path)
|
||||
# Parse using markitdown, passing an explicit charset hint for text
|
||||
# files to avoid markitdown's sample-based (and crash-prone) detection.
|
||||
result = self._markitdown.convert(tmp_path, stream_info=self._utf8_stream_info(file_data, filename))
|
||||
|
||||
if not result or not result.text_content:
|
||||
raise RuntimeError(f"No content extracted from '{filename}'")
|
||||
@@ -153,6 +176,23 @@ class MarkitdownParser(FileParser):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _utf8_stream_info(file_data: bytes, filename: str) -> "StreamInfo | None":
|
||||
"""Return a UTF-8 charset hint for text files that decode cleanly as UTF-8.
|
||||
|
||||
Returns None for binary files or non-UTF-8 text so markitdown falls back
|
||||
to its own detection.
|
||||
"""
|
||||
if Path(filename).suffix.lower() not in _TEXT_EXTENSIONS:
|
||||
return None
|
||||
try:
|
||||
file_data.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
from markitdown import StreamInfo
|
||||
|
||||
return StreamInfo(charset="utf-8")
|
||||
|
||||
@staticmethod
|
||||
def _is_image_file(filename: str) -> bool:
|
||||
"""Return whether the file type needs OCR to extract useful text."""
|
||||
|
||||
@@ -119,12 +119,14 @@ class ClaudeCodeLLM(LLMInterface):
|
||||
Raises:
|
||||
RuntimeError: If the connection test fails.
|
||||
"""
|
||||
from ...config import get_config
|
||||
|
||||
try:
|
||||
test_messages = [{"role": "user", "content": "test"}]
|
||||
await self.call(
|
||||
messages=test_messages,
|
||||
max_completion_tokens=10,
|
||||
temperature=0.0,
|
||||
temperature=get_config().llm_temperature_verification,
|
||||
scope="verification",
|
||||
max_retries=0,
|
||||
)
|
||||
|
||||
@@ -71,6 +71,7 @@ class LiteLLMLLM(LLMInterface):
|
||||
timeout: float | None = None,
|
||||
extra_body: dict[str, Any] | None = None,
|
||||
bedrock_service_tier: str | None = None,
|
||||
default_headers: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(provider, api_key, base_url, model, reasoning_effort, **kwargs)
|
||||
@@ -84,6 +85,13 @@ class LiteLLMLLM(LLMInterface):
|
||||
# drops any the target model rejects (litellm.drop_params=True below).
|
||||
# Sourced from llm_extra_body (env: HINDSIGHT_API_LLM_EXTRA_BODY).
|
||||
self._extra_body: dict[str, Any] = extra_body or {}
|
||||
# Operator-configured default headers forwarded to litellm.acompletion as
|
||||
# ``extra_headers`` (used by deployments routing through proxies / request-
|
||||
# tracing middleware). Mirrors the Anthropic provider's default_headers
|
||||
# wiring. Sourced from llm_default_headers (env: HINDSIGHT_API_LLM_DEFAULT_HEADERS).
|
||||
# Copied so a caller-owned dict can't be mutated through us, and a fresh
|
||||
# copy is handed to each call below to avoid cross-request contamination.
|
||||
self._default_headers: dict[str, Any] = dict(default_headers or {})
|
||||
self.bedrock_service_tier = bedrock_service_tier
|
||||
|
||||
try:
|
||||
@@ -100,12 +108,14 @@ class LiteLLMLLM(LLMInterface):
|
||||
raise RuntimeError("LiteLLM SDK not installed. Run: uv add litellm or pip install litellm") from e
|
||||
|
||||
async def verify_connection(self) -> None:
|
||||
from ...config import get_config
|
||||
|
||||
try:
|
||||
test_messages = [{"role": "user", "content": "test"}]
|
||||
await self.call(
|
||||
messages=test_messages,
|
||||
max_completion_tokens=50,
|
||||
temperature=0.0,
|
||||
temperature=get_config().llm_temperature_verification,
|
||||
scope="verification",
|
||||
max_retries=0,
|
||||
)
|
||||
@@ -144,6 +154,13 @@ class LiteLLMLLM(LLMInterface):
|
||||
for key, value in self._extra_body.items():
|
||||
kwargs.setdefault(key, value)
|
||||
|
||||
# Forward operator-configured default headers as ``extra_headers`` so they
|
||||
# reach the provider behind LiteLLM (proxies / request-tracing middleware).
|
||||
# ``setdefault`` keeps any explicit per-call ``extra_headers`` authoritative;
|
||||
# a per-call copy prevents LiteLLM/downstream from mutating the stored dict.
|
||||
if self._default_headers:
|
||||
kwargs.setdefault("extra_headers", dict(self._default_headers))
|
||||
|
||||
# Bedrock service tier: flex (50% cheaper), priority, or reserved
|
||||
if self.model.startswith("bedrock/") and self.bedrock_service_tier is not None:
|
||||
kwargs["service_tier"] = self.bedrock_service_tier
|
||||
@@ -408,6 +425,14 @@ class LiteLLMLLM(LLMInterface):
|
||||
self._acompletion(**call_kwargs),
|
||||
timeout=self.timeout,
|
||||
)
|
||||
# Stash usage before the tool-call argument parse below, which
|
||||
# can raise json.JSONDecodeError locally even though the provider
|
||||
# already billed for these tokens; without this the error trace
|
||||
# records 0/0 tokens (#2387). Mirrors call() and the anthropic/
|
||||
# gemini call_with_tools paths so the litellm tool path (and the
|
||||
# LiteLLMRouterLLM subclass that inherits this method) completes
|
||||
# the #2396 usage-on-error coverage.
|
||||
stash_response_usage(_usage_from_litellm_response(response))
|
||||
|
||||
message = response.choices[0].message
|
||||
content = message.content
|
||||
|
||||
@@ -146,16 +146,28 @@ class LiteLLMRouterLLM(LiteLLMLLM):
|
||||
kwargs["max_completion_tokens"] = self._cap_max_completion_tokens(max_completion_tokens)
|
||||
if temperature is not None:
|
||||
kwargs["temperature"] = temperature
|
||||
|
||||
# Forward operator-configured default headers as ``extra_headers`` so they
|
||||
# reach the provider behind the Router (proxies / request-tracing middleware).
|
||||
# This override deliberately omits api_key/base_url/extra_body (those live in
|
||||
# the per-deployment Router config), but headers are a cross-cutting operator
|
||||
# concern, so we inject them here too — mirroring the base provider.
|
||||
# ``setdefault`` keeps any explicit per-call ``extra_headers`` authoritative;
|
||||
# a per-call copy prevents LiteLLM/downstream from mutating the stored dict.
|
||||
if self._default_headers:
|
||||
kwargs.setdefault("extra_headers", dict(self._default_headers))
|
||||
return kwargs
|
||||
|
||||
async def verify_connection(self) -> None:
|
||||
from hindsight_api.engine.llm_interface import OutputTooLongError
|
||||
|
||||
from ...config import get_config
|
||||
|
||||
try:
|
||||
await self.call(
|
||||
messages=[{"role": "user", "content": "test"}],
|
||||
max_completion_tokens=50,
|
||||
temperature=0.0,
|
||||
temperature=get_config().llm_temperature_verification,
|
||||
scope="verification",
|
||||
max_retries=0,
|
||||
)
|
||||
|
||||
@@ -101,7 +101,7 @@ class MockLLM(LLMInterface):
|
||||
messages: List of message dicts with 'role' and 'content'.
|
||||
response_format: Optional Pydantic model for structured output.
|
||||
max_completion_tokens: Not used in mock.
|
||||
temperature: Not used in mock.
|
||||
temperature: Recorded on the call record for test assertions.
|
||||
scope: Scope identifier for tracking.
|
||||
max_retries: Not used in mock.
|
||||
initial_backoff: Not used in mock.
|
||||
@@ -123,6 +123,9 @@ class MockLLM(LLMInterface):
|
||||
if response_format and hasattr(response_format, "__name__")
|
||||
else str(response_format),
|
||||
"scope": scope,
|
||||
# Record the temperature so tests can assert per-operation temperature
|
||||
# wiring (None means the parameter was omitted from the call).
|
||||
"temperature": temperature,
|
||||
}
|
||||
self._mock_calls.append(call_record)
|
||||
logger.debug(f"Mock LLM call recorded: scope={scope}, model={self.model}")
|
||||
@@ -208,7 +211,7 @@ class MockLLM(LLMInterface):
|
||||
messages: List of message dicts. Can include tool results with role='tool'.
|
||||
tools: List of tool definitions in OpenAI format.
|
||||
max_completion_tokens: Not used in mock.
|
||||
temperature: Not used in mock.
|
||||
temperature: Recorded on the call record for test assertions.
|
||||
scope: Scope identifier for tracking.
|
||||
max_retries: Not used in mock.
|
||||
initial_backoff: Not used in mock.
|
||||
@@ -225,6 +228,9 @@ class MockLLM(LLMInterface):
|
||||
"messages": messages,
|
||||
"tools": [t.get("function", {}).get("name") for t in tools],
|
||||
"scope": scope,
|
||||
# Record the temperature so tests can assert per-operation temperature
|
||||
# wiring (None means the parameter was omitted from the call).
|
||||
"temperature": temperature,
|
||||
}
|
||||
self._mock_calls.append(call_record)
|
||||
|
||||
|
||||
@@ -90,12 +90,87 @@ _LEAKED_JSON_SUFFIX = re.compile(
|
||||
r'\s*```(?:json)?\s*\{[^}]*(?:"(?:observation_ids|memory_ids|mental_model_ids)"|\})\s*```\s*$',
|
||||
re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
_LEAKED_JSON_OBJECT = re.compile(
|
||||
r'\s*\{[^{]*"(?:observation_ids|memory_ids|mental_model_ids|answer)"[^}]*\}\s*$', re.DOTALL
|
||||
)
|
||||
_TRAILING_IDS_PATTERN = re.compile(
|
||||
r"\s*(?:observation_ids|memory_ids|mental_model_ids)\s*[=:]\s*\[.*?\]\s*$", re.DOTALL | re.IGNORECASE
|
||||
)
|
||||
_JSON_CODE_FENCE_PATTERN = re.compile(r"^\s*```(?:json)?\s*(\{.*\})\s*```\s*$", re.DOTALL | re.IGNORECASE)
|
||||
|
||||
_DONE_ARGUMENT_KEYS = frozenset(
|
||||
{
|
||||
"answer",
|
||||
"directive_compliance",
|
||||
"memory_ids",
|
||||
"mental_model_ids",
|
||||
"observation_ids",
|
||||
"model_ids",
|
||||
}
|
||||
)
|
||||
_DONE_ARGUMENT_MARKER_KEYS = _DONE_ARGUMENT_KEYS - {"answer"}
|
||||
_LEAKED_JSON_ID_KEYS = frozenset({"memory_ids", "mental_model_ids", "observation_ids", "model_ids"})
|
||||
|
||||
|
||||
def _unwrap_leaked_done_arguments(text: str) -> str | None:
|
||||
"""Return the answer when a done tool call was rendered as JSON text.
|
||||
|
||||
Some providers leak the done tool's argument object instead of surfacing it
|
||||
as a native tool call, e.g. {"answer": "...", "memory_ids": [...]}. Only
|
||||
unwrap objects that match the done argument shape so normal JSON answers
|
||||
stay intact.
|
||||
"""
|
||||
candidate = text.strip()
|
||||
if not candidate:
|
||||
return None
|
||||
|
||||
fenced = _JSON_CODE_FENCE_PATTERN.match(candidate)
|
||||
if fenced:
|
||||
candidate = fenced.group(1).strip()
|
||||
|
||||
try:
|
||||
payload = json.loads(candidate)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
answer = payload.get("answer")
|
||||
if not isinstance(answer, str) or not answer.strip():
|
||||
return None
|
||||
|
||||
keys = set(payload)
|
||||
if not keys.intersection(_DONE_ARGUMENT_MARKER_KEYS):
|
||||
return None
|
||||
if not keys.issubset(_DONE_ARGUMENT_KEYS):
|
||||
return None
|
||||
|
||||
for key in ("memory_ids", "mental_model_ids", "observation_ids", "model_ids"):
|
||||
value = payload.get(key)
|
||||
if value is not None and not isinstance(value, list):
|
||||
return None
|
||||
|
||||
return answer.strip()
|
||||
|
||||
|
||||
def _strip_trailing_id_json_object(text: str) -> str:
|
||||
stripped = text.rstrip()
|
||||
if not stripped.endswith("}"):
|
||||
return text.strip()
|
||||
|
||||
start = stripped.rfind("{")
|
||||
if start < 0:
|
||||
return text.strip()
|
||||
|
||||
try:
|
||||
payload = json.loads(stripped[start:])
|
||||
except json.JSONDecodeError:
|
||||
return text.strip()
|
||||
|
||||
if not isinstance(payload, dict) or not payload:
|
||||
return text.strip()
|
||||
keys = set(payload)
|
||||
if not keys.issubset(_LEAKED_JSON_ID_KEYS):
|
||||
return text.strip()
|
||||
|
||||
return stripped[:start].strip()
|
||||
|
||||
|
||||
def _clean_answer_text(text: str) -> str:
|
||||
@@ -104,6 +179,10 @@ def _clean_answer_text(text: str) -> str:
|
||||
Some LLMs output the done() call as text instead of a proper tool call.
|
||||
This strips out patterns like: done({"answer": "...", ...})
|
||||
"""
|
||||
unwrapped = _unwrap_leaked_done_arguments(text)
|
||||
if unwrapped is not None:
|
||||
return unwrapped
|
||||
|
||||
# Remove done() call pattern from the end of the text
|
||||
cleaned = _DONE_CALL_PATTERN.sub("", text).strip()
|
||||
return cleaned if cleaned else text
|
||||
@@ -122,13 +201,17 @@ def _clean_done_answer(text: str) -> str:
|
||||
if not text:
|
||||
return text
|
||||
|
||||
unwrapped = _unwrap_leaked_done_arguments(text)
|
||||
if unwrapped is not None:
|
||||
return unwrapped
|
||||
|
||||
cleaned = text
|
||||
|
||||
# Remove leaked JSON in code blocks at the end
|
||||
cleaned = _LEAKED_JSON_SUFFIX.sub("", cleaned).strip()
|
||||
|
||||
# Remove leaked raw JSON objects at the end
|
||||
cleaned = _LEAKED_JSON_OBJECT.sub("", cleaned).strip()
|
||||
cleaned = _strip_trailing_id_json_object(cleaned)
|
||||
|
||||
# Remove trailing ID patterns
|
||||
cleaned = _TRAILING_IDS_PATTERN.sub("", cleaned).strip()
|
||||
|
||||
@@ -243,7 +243,7 @@ class MemoryFact(BaseModel):
|
||||
|
||||
id: str = Field(description="Unique identifier for the memory fact")
|
||||
text: str = Field(description="The actual text content of the memory")
|
||||
fact_type: str = Field(description="Type of fact: 'world', 'experience', 'opinion', or 'observation'")
|
||||
fact_type: str = Field(description="Type of fact: 'world', 'experience', or 'observation'")
|
||||
entities: list[str] | None = Field(None, description="Entity names mentioned in this fact")
|
||||
context: str | None = Field(None, description="Additional context for the memory")
|
||||
occurred_start: str | None = Field(None, description="ISO format date when the event started occurring")
|
||||
|
||||
@@ -193,7 +193,7 @@ class ExtractedFact(BaseModel):
|
||||
occurred_start: str | None = Field(default=None, description="ISO timestamp for events")
|
||||
occurred_end: str | None = Field(default=None, description="ISO timestamp for event end")
|
||||
fact_type: Literal["world", "assistant"] = Field(
|
||||
description="'world' = objective/external facts. 'assistant' = first-person actions, experiences, or observations by the speaker."
|
||||
description="'world' = objective/external facts, including user preferences, rules, corrections, and constraints even when stated during a conversation. 'assistant' = actions, experiences, or observations the assistant/agent actually performed."
|
||||
)
|
||||
entities: list[Entity] | None = Field(default=None, description="People, places, concepts")
|
||||
causal_relations: list[FactCausalRelation] | None = Field(
|
||||
@@ -296,7 +296,7 @@ class ExtractedFactVerbose(BaseModel):
|
||||
)
|
||||
|
||||
fact_type: Literal["world", "assistant"] = Field(
|
||||
description="'world' = objective/external facts about other people, events, general knowledge. 'assistant' = first-person actions, experiences, or observations by the speaker (e.g., 'I changed X', 'I discovered Y')."
|
||||
description="'world' = objective/external facts about the user, other people, events, general knowledge, preferences, rules, corrections, or constraints. 'assistant' = actions, experiences, or observations the assistant/agent actually performed (e.g., 'I changed X', 'I discovered Y')."
|
||||
)
|
||||
|
||||
entities: list[Entity] | None = Field(
|
||||
@@ -346,7 +346,7 @@ class ExtractedFactNoCausal(BaseModel):
|
||||
occurred_start: str | None = Field(default=None, description="WHEN the event happened (ISO timestamp).")
|
||||
occurred_end: str | None = Field(default=None, description="WHEN the event ended (ISO timestamp).")
|
||||
fact_type: Literal["world", "assistant"] = Field(
|
||||
description="'world' = about the user/others. 'assistant' = experience with assistant."
|
||||
description="'world' = about the user/others, including user preferences, rules, corrections, and constraints. 'assistant' = actions or experiences the assistant/agent actually performed."
|
||||
)
|
||||
entities: list[Entity] | None = Field(
|
||||
default=None,
|
||||
@@ -663,8 +663,8 @@ fact_kind:
|
||||
- "conversation": Ongoing state, preference, trait (no dates)
|
||||
|
||||
fact_type:
|
||||
- "world": About other people, external events, general knowledge, objective facts
|
||||
- "assistant": First-person actions, experiences, or observations by the speaker/author (e.g., "I changed X", "I discovered Y", "I debugged Z"). Also includes interactions with the user (requests, recommendations). If the narrator describes something they did, tried, learned, or decided — use "assistant".
|
||||
- "world": Objective/external facts, including the user's preferences, rules, corrections, constraints, plans, traits, or context. These stay "world" even when the user states them during an assistant interaction (e.g., "User prefers browser_navigate over web_search", "User corrected the project deadline").
|
||||
- "assistant": Actions, experiences, or observations the assistant/agent actually performed (e.g., "I changed X", "I discovered Y", "I debugged Z"). Use this for the assistant/agent doing, trying, learning, deciding, recommending, or responding — not merely for user facts mentioned in conversation.
|
||||
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
TEMPORAL HANDLING
|
||||
@@ -766,7 +766,7 @@ RULES:
|
||||
- Extract all entities (people, places, organizations, objects, concepts).
|
||||
- Extract temporal information (occurred_start, occurred_end, fact_kind, when).
|
||||
- Extract location (where) and people (who).
|
||||
- fact_type: use "world" unless the content is clearly an interaction with the assistant."""
|
||||
- fact_type: use "world" for user preferences, rules, corrections, constraints, traits, and other objective facts, even when stated during an assistant interaction. Use "assistant" only for actions or experiences the assistant/agent actually performed."""
|
||||
|
||||
VERBATIM_FACT_EXTRACTION_PROMPT = _BASE_FACT_EXTRACTION_PROMPT.format(
|
||||
retain_mission_section="{retain_mission_section}",
|
||||
@@ -867,8 +867,8 @@ For CONVERSATIONS (fact_kind="conversation"):
|
||||
FACT TYPE
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
- **world**: User's life, other people, events (would exist without this conversation)
|
||||
- **assistant**: Interactions with assistant (requests, recommendations, help)
|
||||
- **world**: User's life, preferences, rules, corrections, constraints, other people, and events (facts that would exist without this conversation)
|
||||
- **assistant**: Actions or experiences the assistant/agent actually performed while helping the user (requests, recommendations, help)
|
||||
⚠️ CRITICAL for assistant facts: ALWAYS capture the user's request/question in the fact!
|
||||
Include: what the user asked, what problem they wanted solved, what context they provided
|
||||
|
||||
@@ -1203,9 +1203,17 @@ def _build_request_body(llm_config, config, prompt: str, user_message: str, resp
|
||||
request_body = {
|
||||
"model": llm_config.model,
|
||||
"messages": [{"role": "system", "content": prompt}, {"role": "user", "content": user_message}],
|
||||
"temperature": 0.1,
|
||||
}
|
||||
|
||||
# Honour the configured retain temperature. ``None`` omits the parameter
|
||||
# entirely (for models like Azure GPT-5.5 that reject explicit temperatures),
|
||||
# mirroring LLMProvider.call, which drops temperature when it is None. The
|
||||
# batch path builds the request body directly instead of going through
|
||||
# LLMProvider.call (#2469 only de-hardcoded the streaming path), so it must
|
||||
# apply the same rule here.
|
||||
if config.llm_temperature_retain is not None:
|
||||
request_body["temperature"] = config.llm_temperature_retain
|
||||
|
||||
# Add max_completion_tokens if configured
|
||||
if config.retain_max_completion_tokens:
|
||||
request_body["max_completion_tokens"] = config.retain_max_completion_tokens
|
||||
@@ -1314,7 +1322,7 @@ async def _extract_facts_from_chunk(
|
||||
messages=[{"role": "system", "content": prompt}, {"role": "user", "content": user_message}],
|
||||
response_format=response_schema,
|
||||
scope="retain_extract_facts",
|
||||
temperature=0.1,
|
||||
temperature=config.llm_temperature_retain,
|
||||
max_completion_tokens=config.retain_max_completion_tokens,
|
||||
max_retries=llm_max_retries,
|
||||
initial_backoff=initial_backoff,
|
||||
@@ -1814,7 +1822,14 @@ async def extract_facts_from_text(
|
||||
total_usage = total_usage + chunk_usage
|
||||
|
||||
if failed_chunks:
|
||||
failed_summary = ", ".join(f"chunk {idx}: {type(err).__name__}" for idx, err in failed_chunks[:5])
|
||||
# Include the exception message — not just the type — so operators
|
||||
# can tell a structured-JSON parse failure apart from a rate limit
|
||||
# apart from a network 5xx, all of which can surface as the same
|
||||
# exception types. The error_message we propagate to the
|
||||
# async_operations row is the only inspection surface a worker-side
|
||||
# failure leaves behind, and a bare "chunk 0: RuntimeError" is not
|
||||
# actionable.
|
||||
failed_summary = ", ".join(f"chunk {idx}: {type(err).__name__}: {err}" for idx, err in failed_chunks[:5])
|
||||
quota_errors = [err for _, err in failed_chunks if isinstance(err, ProviderRateLimitResetError)]
|
||||
if quota_errors and len(quota_errors) == len(failed_chunks):
|
||||
retry_at = max(err.retry_at for err in quota_errors)
|
||||
|
||||
@@ -5,6 +5,7 @@ Think operation utilities for formulating answers based on agent and world facts
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from ...config import get_config
|
||||
from ..response_models import DispositionTraits, MemoryFact
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -251,7 +252,7 @@ async def reflect(
|
||||
answer_text = await llm_config.call(
|
||||
messages=[{"role": "system", "content": system_message}, {"role": "user", "content": prompt}],
|
||||
scope="memory_think",
|
||||
temperature=0.9,
|
||||
temperature=get_config().llm_temperature_reflect,
|
||||
max_completion_tokens=1000,
|
||||
)
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ from hindsight_api.extensions.operation_validator import (
|
||||
BankListContext,
|
||||
BankListResult,
|
||||
BankReadContext,
|
||||
BankReadOperation,
|
||||
BankWriteContext,
|
||||
BankWriteOperation,
|
||||
# Consolidation operation
|
||||
ConsolidateContext,
|
||||
ConsolidateResult,
|
||||
@@ -54,6 +56,7 @@ from hindsight_api.extensions.operation_validator import (
|
||||
OperationValidationError,
|
||||
OperationValidatorExtension,
|
||||
PrecheckContext,
|
||||
PrecheckOperation,
|
||||
RecallContext,
|
||||
RecallResult,
|
||||
ReflectContext,
|
||||
@@ -87,6 +90,7 @@ __all__ = [
|
||||
"OperationValidationError",
|
||||
"OperationValidatorExtension",
|
||||
"PrecheckContext",
|
||||
"PrecheckOperation",
|
||||
"RecallContext",
|
||||
"RecallResult",
|
||||
"ReflectContext",
|
||||
@@ -98,7 +102,9 @@ __all__ = [
|
||||
"BankListContext",
|
||||
"BankListResult",
|
||||
"BankReadContext",
|
||||
"BankReadOperation",
|
||||
"BankWriteContext",
|
||||
"BankWriteOperation",
|
||||
# Operation Validator - Consolidation
|
||||
"ConsolidateContext",
|
||||
"ConsolidateResult",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from hindsight_api.extensions.base import Extension
|
||||
@@ -82,6 +83,18 @@ class ValidationResult:
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class PrecheckOperation(StrEnum):
|
||||
"""Route operation names passed to the pre-body-parse precheck hook."""
|
||||
|
||||
DRY_RUN_EXTRACT = "dry_run_extract"
|
||||
FILES_RETAIN = "files_retain"
|
||||
MENTAL_MODEL_CREATE = "mental_model_create"
|
||||
MENTAL_MODEL_REFRESH = "mental_model_refresh"
|
||||
RECALL = "recall"
|
||||
REFLECT = "reflect"
|
||||
RETAIN = "retain"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrecheckContext:
|
||||
"""Context for a pre-body-parse precheck on an operation.
|
||||
@@ -91,9 +104,7 @@ class PrecheckContext:
|
||||
therefore intentionally carries only the cheap, already-resolved
|
||||
pieces of request state:
|
||||
|
||||
- ``operation``: a short string identifying the route, e.g. ``"retain"``,
|
||||
``"recall"``, ``"reflect"``, ``"files_retain"``, ``"mental_model_create"``,
|
||||
``"mental_model_refresh"``.
|
||||
- ``operation``: a short string-compatible enum identifying the route.
|
||||
- ``bank_id``: parsed from the URL path.
|
||||
- ``request_context``: the authenticated :class:`RequestContext` (tenant
|
||||
already resolved by the tenant extension).
|
||||
@@ -108,7 +119,7 @@ class PrecheckContext:
|
||||
the source of truth for the precise per-call cost / quota arithmetic.
|
||||
"""
|
||||
|
||||
operation: str
|
||||
operation: PrecheckOperation
|
||||
bank_id: str
|
||||
request_context: "RequestContext"
|
||||
content_length: int | None = None
|
||||
@@ -303,12 +314,77 @@ class ConsolidateResult:
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class BankReadOperation(StrEnum):
|
||||
"""Bank-scoped read operation names passed to validate_bank_read."""
|
||||
|
||||
GET_BANK_CONFIG = "get_bank_config"
|
||||
GET_BANK_PROFILE = "get_bank_profile"
|
||||
GET_BANK_STATS = "get_bank_stats"
|
||||
GET_CHUNK = "get_chunk"
|
||||
GET_DIRECTIVE = "get_directive"
|
||||
GET_DOCUMENT = "get_document"
|
||||
GET_ENTITY = "get_entity"
|
||||
GET_ENTITY_GRAPH = "get_entity_graph"
|
||||
GET_ENTITY_STATE = "get_entity_state"
|
||||
GET_GRAPH_DATA = "get_graph_data"
|
||||
GET_MEMORIES_TIMESERIES = "get_memories_timeseries"
|
||||
GET_MEMORY_UNIT = "get_memory_unit"
|
||||
GET_OBSERVATION_HISTORY = "get_observation_history"
|
||||
GET_OPERATION_STATUS = "get_operation_status"
|
||||
LIST_DIRECTIVES = "list_directives"
|
||||
LIST_DOCUMENT_CHUNKS = "list_document_chunks"
|
||||
LIST_DOCUMENTS = "list_documents"
|
||||
LIST_ENTITIES = "list_entities"
|
||||
LIST_MEMORY_UNITS = "list_memory_units"
|
||||
LIST_MENTAL_MODEL_TAGS = "list_mental_model_tags"
|
||||
LIST_MENTAL_MODELS = "list_mental_models"
|
||||
LIST_OBSERVATION_SCOPES = "list_observation_scopes"
|
||||
LIST_OPERATIONS = "list_operations"
|
||||
LIST_TAGS = "list_tags"
|
||||
LIST_WEBHOOK_DELIVERIES = "list_webhook_deliveries"
|
||||
LIST_WEBHOOKS = "list_webhooks"
|
||||
|
||||
|
||||
class BankWriteOperation(StrEnum):
|
||||
"""Bank-scoped write operation names passed to validate_bank_write."""
|
||||
|
||||
CANCEL_OPERATION = "cancel_operation"
|
||||
CLEAR_MENTAL_MODEL = "clear_mental_model"
|
||||
CLEAR_OBSERVATIONS = "clear_observations"
|
||||
CLEAR_OBSERVATIONS_FOR_MEMORY = "clear_observations_for_memory"
|
||||
CREATE_DIRECTIVE = "create_directive"
|
||||
CREATE_MENTAL_MODEL = "create_mental_model"
|
||||
CREATE_WEBHOOK = "create_webhook"
|
||||
DELETE_BANK = "delete_bank"
|
||||
DELETE_DIRECTIVE = "delete_directive"
|
||||
DELETE_DOCUMENT = "delete_document"
|
||||
DELETE_MENTAL_MODEL = "delete_mental_model"
|
||||
DELETE_WEBHOOK = "delete_webhook"
|
||||
MERGE_BANK_MISSION = "merge_bank_mission"
|
||||
REPROCESS_DOCUMENT = "reprocess_document"
|
||||
RESET_BANK_CONFIG = "reset_bank_config"
|
||||
RETRY_FAILED_CONSOLIDATION = "retry_failed_consolidation"
|
||||
RETRY_OPERATION = "retry_operation"
|
||||
RUN_CONSOLIDATION = "run_consolidation"
|
||||
SET_BANK_MISSION = "set_bank_mission"
|
||||
SUBMIT_ASYNC_CONSOLIDATION = "submit_async_consolidation"
|
||||
SUBMIT_ASYNC_GRAPH_MAINTENANCE = "submit_async_graph_maintenance"
|
||||
UPDATE_BANK = "update_bank"
|
||||
UPDATE_BANK_CONFIG = "update_bank_config"
|
||||
UPDATE_BANK_DISPOSITION = "update_bank_disposition"
|
||||
UPDATE_DIRECTIVE = "update_directive"
|
||||
UPDATE_DOCUMENT = "update_document"
|
||||
UPDATE_MEMORY_UNIT = "update_memory_unit"
|
||||
UPDATE_MENTAL_MODEL = "update_mental_model"
|
||||
UPDATE_WEBHOOK = "update_webhook"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BankReadContext:
|
||||
"""Context for a bank read operation validation (pre-operation)."""
|
||||
|
||||
bank_id: str
|
||||
operation: str # "get_bank_profile", "get_bank_stats"
|
||||
operation: BankReadOperation
|
||||
request_context: "RequestContext"
|
||||
|
||||
|
||||
@@ -317,7 +393,7 @@ class BankWriteContext:
|
||||
"""Context for a bank write operation validation (pre-operation)."""
|
||||
|
||||
bank_id: str
|
||||
operation: str # "delete_bank", "update_bank", "update_bank_disposition", "set_bank_mission", "merge_bank_mission", "clear_observations", "clear_observations_for_memory"
|
||||
operation: BankWriteOperation
|
||||
request_context: "RequestContext"
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from sqlalchemy.pool import NullPool
|
||||
from ._pg_search import normalize_pg_search_tokenizer, pg_search_bm25_columns
|
||||
from ._vector_index import (
|
||||
bootstrap_extension,
|
||||
configured_vector_extension,
|
||||
detect_vector_extension,
|
||||
index_type_keyword,
|
||||
index_using_clause,
|
||||
@@ -60,6 +61,86 @@ def _detect_vector_extension(conn, vector_extension: str = "pgvector") -> str:
|
||||
return detect_vector_extension(conn, vector_extension)
|
||||
|
||||
|
||||
def _ensure_pgvector_extension_in_public(conn: Connection) -> None:
|
||||
"""Ensure pgvector is installed before pgvector-backed migrations run."""
|
||||
logger.debug("Checking pgvector extension availability...")
|
||||
|
||||
# First, check if extension already exists
|
||||
ext_check = conn.execute(
|
||||
text(
|
||||
"SELECT extname, nspname FROM pg_extension e "
|
||||
"JOIN pg_namespace n ON e.extnamespace = n.oid "
|
||||
"WHERE extname = 'vector'"
|
||||
)
|
||||
).fetchone()
|
||||
|
||||
if ext_check:
|
||||
# Extension exists - check if in correct schema
|
||||
ext_schema = ext_check[1]
|
||||
if ext_schema == "public":
|
||||
logger.info("pgvector extension found in public schema - ready to use")
|
||||
else:
|
||||
# Extension in wrong schema - try to fix if we have permissions
|
||||
logger.warning(
|
||||
f"pgvector extension found in schema '{ext_schema}' instead of 'public'. Attempting to relocate..."
|
||||
)
|
||||
try:
|
||||
conn.execute(text("DROP EXTENSION vector CASCADE"))
|
||||
conn.execute(text("SET search_path TO public"))
|
||||
conn.execute(text("CREATE EXTENSION vector"))
|
||||
conn.commit()
|
||||
logger.info("pgvector extension relocated to public schema")
|
||||
except Exception as e:
|
||||
# Failed to relocate - log but don't fail if extension exists somewhere
|
||||
logger.warning(
|
||||
f"Could not relocate pgvector extension to public schema: {e}. "
|
||||
f"Continuing with extension in '{ext_schema}' schema."
|
||||
)
|
||||
conn.rollback()
|
||||
else:
|
||||
# Extension doesn't exist - try to install
|
||||
logger.info("pgvector extension not found, attempting to install...")
|
||||
try:
|
||||
conn.execute(text("SET search_path TO public"))
|
||||
conn.execute(text("CREATE EXTENSION vector"))
|
||||
conn.commit()
|
||||
logger.info("pgvector extension installed in public schema")
|
||||
except Exception as e:
|
||||
# Installation failed - this is only fatal if extension truly doesn't exist
|
||||
# Check one more time in case another process installed it
|
||||
conn.rollback()
|
||||
ext_recheck = conn.execute(
|
||||
text(
|
||||
"SELECT nspname FROM pg_extension e "
|
||||
"JOIN pg_namespace n ON e.extnamespace = n.oid "
|
||||
"WHERE extname = 'vector'"
|
||||
)
|
||||
).fetchone()
|
||||
|
||||
if ext_recheck:
|
||||
logger.warning(
|
||||
f"Could not install pgvector extension (permission denied?), "
|
||||
f"but extension exists in '{ext_recheck[0]}' schema. Continuing..."
|
||||
)
|
||||
else:
|
||||
# Extension truly doesn't exist and we can't install it
|
||||
logger.error(
|
||||
f"pgvector extension is not installed and cannot be installed: {e}. "
|
||||
f"Please ensure pgvector is installed by a database administrator. "
|
||||
f"See: https://github.com/pgvector/pgvector#installation"
|
||||
)
|
||||
raise RuntimeError(
|
||||
"pgvector extension is required but not installed. Please install it with: CREATE EXTENSION vector;"
|
||||
) from e
|
||||
|
||||
|
||||
def _bootstrap_vector_extension_for_migrations(conn: Connection, vector_extension: str) -> None:
|
||||
"""Bootstrap the configured vector backend before schema migrations run."""
|
||||
if vector_extension == "pgvector":
|
||||
_ensure_pgvector_extension_in_public(conn)
|
||||
bootstrap_extension(conn, vector_extension)
|
||||
|
||||
|
||||
def _drop_per_bank_vector_indexes(conn: Connection, schema_name: str) -> None:
|
||||
"""Drop per-bank partial memory_units vector indexes after global ScaNN is ready."""
|
||||
rows = conn.execute(
|
||||
@@ -275,83 +356,8 @@ def run_migrations(
|
||||
logger.debug("Migration advisory lock acquired")
|
||||
|
||||
try:
|
||||
# Ensure pgvector extension is installed globally BEFORE schema migrations
|
||||
# This is critical: the extension must exist database-wide before any schema
|
||||
# migrations run, otherwise custom schemas won't have access to vector types
|
||||
logger.debug("Checking pgvector extension availability...")
|
||||
|
||||
# First, check if extension already exists
|
||||
ext_check = conn.execute(
|
||||
text(
|
||||
"SELECT extname, nspname FROM pg_extension e "
|
||||
"JOIN pg_namespace n ON e.extnamespace = n.oid "
|
||||
"WHERE extname = 'vector'"
|
||||
)
|
||||
).fetchone()
|
||||
|
||||
if ext_check:
|
||||
# Extension exists - check if in correct schema
|
||||
ext_schema = ext_check[1]
|
||||
if ext_schema == "public":
|
||||
logger.info("pgvector extension found in public schema - ready to use")
|
||||
else:
|
||||
# Extension in wrong schema - try to fix if we have permissions
|
||||
logger.warning(
|
||||
f"pgvector extension found in schema '{ext_schema}' instead of 'public'. "
|
||||
f"Attempting to relocate..."
|
||||
)
|
||||
try:
|
||||
conn.execute(text("DROP EXTENSION vector CASCADE"))
|
||||
conn.execute(text("SET search_path TO public"))
|
||||
conn.execute(text("CREATE EXTENSION vector"))
|
||||
conn.commit()
|
||||
logger.info("pgvector extension relocated to public schema")
|
||||
except Exception as e:
|
||||
# Failed to relocate - log but don't fail if extension exists somewhere
|
||||
logger.warning(
|
||||
f"Could not relocate pgvector extension to public schema: {e}. "
|
||||
f"Continuing with extension in '{ext_schema}' schema."
|
||||
)
|
||||
conn.rollback()
|
||||
else:
|
||||
# Extension doesn't exist - try to install
|
||||
logger.info("pgvector extension not found, attempting to install...")
|
||||
try:
|
||||
conn.execute(text("SET search_path TO public"))
|
||||
conn.execute(text("CREATE EXTENSION vector"))
|
||||
conn.commit()
|
||||
logger.info("pgvector extension installed in public schema")
|
||||
except Exception as e:
|
||||
# Installation failed - this is only fatal if extension truly doesn't exist
|
||||
# Check one more time in case another process installed it
|
||||
conn.rollback()
|
||||
ext_recheck = conn.execute(
|
||||
text(
|
||||
"SELECT nspname FROM pg_extension e "
|
||||
"JOIN pg_namespace n ON e.extnamespace = n.oid "
|
||||
"WHERE extname = 'vector'"
|
||||
)
|
||||
).fetchone()
|
||||
|
||||
if ext_recheck:
|
||||
logger.warning(
|
||||
f"Could not install pgvector extension (permission denied?), "
|
||||
f"but extension exists in '{ext_recheck[0]}' schema. Continuing..."
|
||||
)
|
||||
else:
|
||||
# Extension truly doesn't exist and we can't install it
|
||||
logger.error(
|
||||
f"pgvector extension is not installed and cannot be installed: {e}. "
|
||||
f"Please ensure pgvector is installed by a database administrator. "
|
||||
f"See: https://github.com/pgvector/pgvector#installation"
|
||||
)
|
||||
raise RuntimeError(
|
||||
"pgvector extension is required but not installed. "
|
||||
"Please install it with: CREATE EXTENSION vector;"
|
||||
) from e
|
||||
|
||||
vector_extension = os.getenv("HINDSIGHT_API_VECTOR_EXTENSION", "pgvector").lower()
|
||||
bootstrap_extension(conn, vector_extension)
|
||||
vector_extension = configured_vector_extension()
|
||||
_bootstrap_vector_extension_for_migrations(conn, vector_extension)
|
||||
|
||||
# Commit any pending transaction on the advisory-lock connection
|
||||
# before running migrations. Some code paths above (e.g., the
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-api-slim"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
description = "Hindsight: Agent Memory That Works Like Human Memory"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -54,6 +54,29 @@ async def _teardown_memory_engine(mem: MemoryEngine) -> None:
|
||||
unregister_span_recorder(mem._llm_recorder)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _cleanup_leaked_span_recorders():
|
||||
"""Fail-safe for the process-global LLM-trace recorder registry (#2229).
|
||||
|
||||
``MemoryEngine.__init__`` registers its recorder in the shared registry, and
|
||||
only ``close()`` removes it. Tests that construct an engine directly (without
|
||||
``_teardown_memory_engine``/``close()``) leak an *enabled* recorder; a later
|
||||
test's LLM calls then get recorded into the shared DB, flaking
|
||||
``test_llm_trace::test_disabled_writes_no_rows`` (it observes rows for its
|
||||
bank even though its own recorder is disabled). ``_teardown_memory_engine``
|
||||
guards the fixtures; this guards everything else by dropping any recorder a
|
||||
test added to the registry.
|
||||
"""
|
||||
from hindsight_api.tracing import get_span_recorder
|
||||
|
||||
recorders = get_span_recorder()._recorders
|
||||
before = {id(r) for r in recorders}
|
||||
yield
|
||||
for recorder in list(recorders):
|
||||
if id(recorder) not in before:
|
||||
recorders.remove(recorder)
|
||||
|
||||
|
||||
# Default pg0 instance configuration for tests
|
||||
DEFAULT_PG0_INSTANCE_NAME = "hindsight-test"
|
||||
DEFAULT_PG0_PORT = int(os.environ.get("HINDSIGHT_TEST_PG_PORT", "5556"))
|
||||
|
||||
@@ -88,7 +88,10 @@ async def test_backup_tables_covers_entire_schema(backup_test_schema):
|
||||
await conn.close()
|
||||
|
||||
# alembic_version is migration bookkeeping, not data — never backed up.
|
||||
schema_tables = {r["table_name"] for r in rows} - {"alembic_version"}
|
||||
# bank_stats_cache is a derived TTL cache of get_bank_stats results: it has no
|
||||
# FK to banks (so the restore cascade never touches it) and repopulates itself
|
||||
# on demand, so it is deliberately not backed up — a restore starts it cold.
|
||||
schema_tables = {r["table_name"] for r in rows} - {"alembic_version", "bank_stats_cache"}
|
||||
backup_tables = set(BACKUP_TABLES)
|
||||
|
||||
missing = schema_tables - backup_tables
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Tests for the table-backed (cross-process) get_bank_stats cache.
|
||||
|
||||
On PostgreSQL the engine backs `get_bank_stats` with the `bank_stats_cache`
|
||||
table (`DistributedBankStatsCache`) instead of a per-process dict, so one
|
||||
worker's computation is shared with every other worker. These tests verify:
|
||||
|
||||
* the PG engine actually selects the distributed cache,
|
||||
* a computed result is written to the table and served from it on the next call,
|
||||
* invalidation deletes the row so the next call recomputes, and
|
||||
* an unreachable cache table degrades to computing without caching rather than
|
||||
failing the endpoint.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api import RequestContext
|
||||
from hindsight_api.engine.bank_stats_cache import DistributedBankStatsCache
|
||||
from hindsight_api.engine.memory_engine import MemoryEngine, get_current_schema
|
||||
|
||||
_PINNED_TTL_SECONDS = 300.0
|
||||
|
||||
|
||||
async def _insert_memory(conn, bank_id: str, text: str, fact_type: str = "experience") -> uuid.UUID:
|
||||
mem_id = uuid.uuid4()
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO memory_units (id, bank_id, text, fact_type, event_date, created_at, updated_at, consolidated_at)
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW(), NOW(), NOW())
|
||||
""",
|
||||
mem_id,
|
||||
bank_id,
|
||||
text,
|
||||
fact_type,
|
||||
)
|
||||
return mem_id
|
||||
|
||||
|
||||
async def _ensure_bank(memory: MemoryEngine, bank_id: str, request_context: RequestContext) -> None:
|
||||
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
|
||||
|
||||
|
||||
def _pin_distributed_cache(memory: MemoryEngine) -> DistributedBankStatsCache:
|
||||
cache = DistributedBankStatsCache(backend=memory._backend, ttl_seconds=_PINNED_TTL_SECONDS)
|
||||
memory._bank_stats_cache = cache
|
||||
return cache
|
||||
|
||||
|
||||
class TestDistributedBankStatsCache:
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_engine_selects_distributed_cache(self, memory: MemoryEngine):
|
||||
if memory._database_backend_type != "postgresql":
|
||||
pytest.skip("distributed cache is PostgreSQL-only")
|
||||
assert isinstance(memory._bank_stats_cache, DistributedBankStatsCache)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_result_is_written_and_served_from_table(self, memory: MemoryEngine, request_context: RequestContext):
|
||||
if memory._database_backend_type != "postgresql":
|
||||
pytest.skip("distributed cache is PostgreSQL-only")
|
||||
|
||||
bank_id = f"test-dist-stats-{uuid.uuid4().hex[:8]}"
|
||||
await _ensure_bank(memory, bank_id, request_context)
|
||||
pool = await memory._get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
await _insert_memory(conn, bank_id, "Alice loves hiking.")
|
||||
|
||||
_pin_distributed_cache(memory)
|
||||
try:
|
||||
first = await memory.get_bank_stats(bank_id, request_context=request_context)
|
||||
assert first["node_counts"].get("experience") == 1
|
||||
|
||||
# The computed result was persisted to the shared table.
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetchval("SELECT count(*) FROM bank_stats_cache WHERE bank_id = $1", bank_id)
|
||||
assert rows == 1
|
||||
|
||||
# Mutate the underlying data WITHOUT going through an invalidating
|
||||
# engine method — the long-TTL cache must serve the stale row.
|
||||
async with pool.acquire() as conn:
|
||||
await _insert_memory(conn, bank_id, "Bob enjoys cycling.")
|
||||
served = await memory.get_bank_stats(bank_id, request_context=request_context)
|
||||
assert served["node_counts"].get("experience") == 1 # still cached
|
||||
|
||||
# Invalidating drops the row → next call recomputes the true count.
|
||||
await memory._bank_stats_cache.invalidate(get_current_schema(), bank_id)
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetchval("SELECT count(*) FROM bank_stats_cache WHERE bank_id = $1", bank_id)
|
||||
assert rows == 0
|
||||
fresh = await memory.get_bank_stats(bank_id, request_context=request_context)
|
||||
assert fresh["node_counts"].get("experience") == 2
|
||||
finally:
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_force_refresh_bypasses_and_updates_cache(
|
||||
self, memory: MemoryEngine, request_context: RequestContext
|
||||
):
|
||||
if memory._database_backend_type != "postgresql":
|
||||
pytest.skip("distributed cache is PostgreSQL-only")
|
||||
|
||||
bank_id = f"test-dist-stats-fresh-{uuid.uuid4().hex[:8]}"
|
||||
await _ensure_bank(memory, bank_id, request_context)
|
||||
pool = await memory._get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
await _insert_memory(conn, bank_id, "Alice loves hiking.")
|
||||
|
||||
_pin_distributed_cache(memory)
|
||||
try:
|
||||
# Warm the cache, then mutate the data without invalidation.
|
||||
assert (await memory.get_bank_stats(bank_id, request_context=request_context))["node_counts"][
|
||||
"experience"
|
||||
] == 1
|
||||
async with pool.acquire() as conn:
|
||||
await _insert_memory(conn, bank_id, "Bob enjoys cycling.")
|
||||
|
||||
# A normal read is served the stale cached count...
|
||||
stale = await memory.get_bank_stats(bank_id, request_context=request_context)
|
||||
assert stale["node_counts"]["experience"] == 1
|
||||
# ...but force_refresh recomputes the true count.
|
||||
fresh = await memory.get_bank_stats(bank_id, request_context=request_context, force_refresh=True)
|
||||
assert fresh["node_counts"]["experience"] == 2
|
||||
# The forced result also refreshed the cache for the next caller.
|
||||
served = await memory.get_bank_stats(bank_id, request_context=request_context)
|
||||
assert served["node_counts"]["experience"] == 2
|
||||
finally:
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_degrades_when_cache_table_unreachable(self, memory: MemoryEngine, request_context: RequestContext):
|
||||
if memory._database_backend_type != "postgresql":
|
||||
pytest.skip("distributed cache is PostgreSQL-only")
|
||||
|
||||
bank_id = f"test-dist-stats-degrade-{uuid.uuid4().hex[:8]}"
|
||||
await _ensure_bank(memory, bank_id, request_context)
|
||||
pool = await memory._get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
await _insert_memory(conn, bank_id, "Alice loves hiking.")
|
||||
|
||||
# Point the cache at a table that does not exist: reads and writes fail,
|
||||
# so it must fall back to computing the real result (no real table touched).
|
||||
cache = _pin_distributed_cache(memory)
|
||||
cache._qualified = lambda schema: '"public".bank_stats_cache_does_not_exist' # type: ignore[method-assign]
|
||||
try:
|
||||
stats = await memory.get_bank_stats(bank_id, request_context=request_context)
|
||||
assert stats["node_counts"].get("experience") == 1
|
||||
finally:
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
@@ -3619,3 +3619,39 @@ def test_consolidation_prompt_split_is_cacheable_and_complete():
|
||||
)
|
||||
assert "OBSERVATION LIMIT REACHED" in capped
|
||||
assert "OBSERVATION LIMIT REACHED" not in sys_prompt
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_observation_populates_search_vector_native(memory, request_context):
|
||||
"""Observations created via consolidation must have search_vector populated
|
||||
when text_search_extension == 'native', so BM25 retrieval finds them."""
|
||||
from hindsight_api.config import get_config
|
||||
|
||||
config = get_config()
|
||||
if config.text_search_extension != "native":
|
||||
pytest.skip("Only applies to native text search backend")
|
||||
|
||||
bank_id = f"test-search-vector-{uuid.uuid4().hex[:8]}"
|
||||
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
|
||||
|
||||
await memory.retain_async(
|
||||
bank_id=bank_id,
|
||||
content="Django uses middleware for request processing.",
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
async with memory._pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT search_vector
|
||||
FROM memory_units
|
||||
WHERE bank_id = $1 AND fact_type = 'observation'
|
||||
LIMIT 1
|
||||
""",
|
||||
bank_id,
|
||||
)
|
||||
|
||||
assert row is not None, "Consolidation should have created an observation"
|
||||
assert row["search_vector"] is not None, "search_vector must be populated for BM25 retrieval under native backend"
|
||||
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
@@ -84,7 +84,13 @@ def _ctx(threshold: float = 0.97):
|
||||
conn=conn,
|
||||
memory_engine=types.SimpleNamespace(embeddings=object()),
|
||||
bank_id="bank1",
|
||||
config=types.SimpleNamespace(consolidation_dedup_threshold=threshold),
|
||||
# The merge path builds a search_vector UPDATE clause from the text-search
|
||||
# config, so these must be present (production defaults: native/english).
|
||||
config=types.SimpleNamespace(
|
||||
consolidation_dedup_threshold=threshold,
|
||||
text_search_extension="native",
|
||||
text_search_extension_native_language="english",
|
||||
),
|
||||
dedup_llm_config=llm,
|
||||
create_text="YouTube content in Uzbek is very rich.",
|
||||
create_source_ids=[uuid.uuid4()],
|
||||
@@ -126,6 +132,16 @@ async def test_dedup_llm_keep_does_not_merge() -> None:
|
||||
conn.execute.assert_not_called() # kept distinct → no merge
|
||||
|
||||
|
||||
async def test_dedup_llm_missing_action_defaults_to_keep() -> None:
|
||||
kwargs, conn, llm = _ctx()
|
||||
llm.call.return_value = _DedupDecision(reason="underfilled structured response")
|
||||
with _patch_embed(), _patch_probe([_obs("Uzbek content on YouTube is described as very rich.", 0.98)]):
|
||||
result = await _dedup_reconcile_create(**kwargs)
|
||||
assert result is None
|
||||
llm.call.assert_awaited_once()
|
||||
conn.execute.assert_not_called() # missing action is a conservative no-merge
|
||||
|
||||
|
||||
async def test_dedup_llm_merge_folds_into_twin() -> None:
|
||||
kwargs, conn, llm = _ctx()
|
||||
kwargs["create_source_ids"] = [uuid.uuid4(), uuid.uuid4()]
|
||||
@@ -169,7 +185,13 @@ def _update_ctx(threshold: float = 0.97):
|
||||
conn=conn,
|
||||
memory_engine=types.SimpleNamespace(embeddings=object()),
|
||||
bank_id="bank1",
|
||||
config=types.SimpleNamespace(consolidation_dedup_threshold=threshold),
|
||||
# The merge path builds a search_vector UPDATE clause from the text-search
|
||||
# config, so these must be present (production defaults: native/english).
|
||||
config=types.SimpleNamespace(
|
||||
consolidation_dedup_threshold=threshold,
|
||||
text_search_extension="native",
|
||||
text_search_extension_native_language="english",
|
||||
),
|
||||
dedup_llm_config=llm,
|
||||
updated_id=_UPDATED_ID,
|
||||
updated_text="Uzbek content on YouTube is very rich and growing.",
|
||||
|
||||
@@ -9,11 +9,19 @@ from fastapi.testclient import TestClient
|
||||
from hindsight_api.extensions import (
|
||||
ApiKeyTenantExtension,
|
||||
AuthenticationError,
|
||||
BankReadContext,
|
||||
BankReadOperation,
|
||||
BankWriteContext,
|
||||
BankWriteOperation,
|
||||
# Consolidation operation
|
||||
ConsolidateContext,
|
||||
ConsolidateResult,
|
||||
Extension,
|
||||
HttpExtension,
|
||||
OperationValidationError,
|
||||
OperationValidatorExtension,
|
||||
PrecheckContext,
|
||||
PrecheckOperation,
|
||||
RecallContext,
|
||||
RecallResult,
|
||||
ReflectContext,
|
||||
@@ -21,13 +29,8 @@ from hindsight_api.extensions import (
|
||||
RequestContext,
|
||||
RetainContext,
|
||||
RetainResult,
|
||||
TenantContext,
|
||||
TenantExtension,
|
||||
ValidationResult,
|
||||
load_extension,
|
||||
# Consolidation operation
|
||||
ConsolidateContext,
|
||||
ConsolidateResult,
|
||||
)
|
||||
|
||||
|
||||
@@ -68,6 +71,36 @@ class TestExtensionLoader:
|
||||
await ext.on_shutdown()
|
||||
assert ext.stopped
|
||||
|
||||
def test_operation_enums_remain_string_compatible(self):
|
||||
"""Operation enums centralize names without breaking string comparisons."""
|
||||
request_context = RequestContext(tenant_id="tenant-1")
|
||||
|
||||
precheck_ctx = PrecheckContext(
|
||||
bank_id="bank-1",
|
||||
operation=PrecheckOperation.RETAIN,
|
||||
request_context=request_context,
|
||||
)
|
||||
read_ctx = BankReadContext(
|
||||
bank_id="bank-1",
|
||||
operation=BankReadOperation.GET_BANK_STATS,
|
||||
request_context=request_context,
|
||||
)
|
||||
write_ctx = BankWriteContext(
|
||||
bank_id="bank-1",
|
||||
operation=BankWriteOperation.UPDATE_BANK_CONFIG,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
assert precheck_ctx.operation is PrecheckOperation.RETAIN
|
||||
assert read_ctx.operation is BankReadOperation.GET_BANK_STATS
|
||||
assert write_ctx.operation is BankWriteOperation.UPDATE_BANK_CONFIG
|
||||
assert precheck_ctx.operation == "retain"
|
||||
assert read_ctx.operation == "get_bank_stats"
|
||||
assert write_ctx.operation == "update_bank_config"
|
||||
assert isinstance(precheck_ctx.operation, str)
|
||||
assert isinstance(read_ctx.operation, str)
|
||||
assert isinstance(write_ctx.operation, str)
|
||||
|
||||
|
||||
class LifecycleTestExtension(Extension):
|
||||
"""Test extension for config and lifecycle tests."""
|
||||
@@ -356,6 +389,7 @@ class TestOperationHooksParameters:
|
||||
async def test_recall_pre_hook_receives_all_parameters(self, memory_with_tracking_validator):
|
||||
"""Pre-recall hook receives all user-provided parameters."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
memory, validator = memory_with_tracking_validator
|
||||
@@ -882,7 +916,7 @@ class TestPrecheckDefault:
|
||||
validator = RecordingPrecheckValidator(reject=False)
|
||||
# Bypass our override by calling the base implementation directly.
|
||||
ctx = PrecheckContext(
|
||||
operation="retain",
|
||||
operation=PrecheckOperation.RETAIN,
|
||||
bank_id="bank-x",
|
||||
request_context=RequestContext(),
|
||||
)
|
||||
@@ -914,7 +948,7 @@ class TestPrecheckHttpWiring:
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
||||
from hindsight_api.extensions import PrecheckContext
|
||||
from hindsight_api.extensions import PrecheckContext, PrecheckOperation
|
||||
from hindsight_api.models import RequestContext
|
||||
|
||||
body_parses: list[str] = []
|
||||
@@ -949,7 +983,7 @@ class TestPrecheckHttpWiring:
|
||||
async def _request_context() -> RequestContext:
|
||||
return RequestContext()
|
||||
|
||||
def _precheck_for(operation: str):
|
||||
def _precheck_for(operation: PrecheckOperation):
|
||||
async def _dep(
|
||||
bank_id: str,
|
||||
request: Request,
|
||||
@@ -985,7 +1019,7 @@ class TestPrecheckHttpWiring:
|
||||
async def retain(
|
||||
bank_id: str,
|
||||
body: _RetainBody,
|
||||
_: None = Depends(_precheck_for("retain")),
|
||||
_: None = Depends(_precheck_for(PrecheckOperation.RETAIN)),
|
||||
):
|
||||
return {"ok": True, "bank_id": bank_id, "n": len(body.items)}
|
||||
|
||||
@@ -993,7 +1027,7 @@ class TestPrecheckHttpWiring:
|
||||
async def recall(
|
||||
bank_id: str,
|
||||
body: _RecallBody,
|
||||
_: None = Depends(_precheck_for("recall")),
|
||||
_: None = Depends(_precheck_for(PrecheckOperation.RECALL)),
|
||||
):
|
||||
return {"ok": True}
|
||||
|
||||
@@ -1001,7 +1035,7 @@ class TestPrecheckHttpWiring:
|
||||
async def reflect(
|
||||
bank_id: str,
|
||||
body: _ReflectBody,
|
||||
_: None = Depends(_precheck_for("reflect")),
|
||||
_: None = Depends(_precheck_for(PrecheckOperation.REFLECT)),
|
||||
):
|
||||
return {"ok": True}
|
||||
|
||||
@@ -1168,7 +1202,7 @@ class TestPrecheckHttpWiring:
|
||||
content_length = parsed
|
||||
|
||||
ctx = PrecheckContext(
|
||||
operation="retain",
|
||||
operation=PrecheckOperation.RETAIN,
|
||||
bank_id="bank-x",
|
||||
request_context=RequestContext(),
|
||||
content_length=content_length,
|
||||
|
||||
@@ -27,6 +27,13 @@ def llm_config():
|
||||
api_key=config.retain_llm_api_key or config.llm_api_key,
|
||||
model=config.retain_llm_model or config.llm_model,
|
||||
base_url=config.retain_llm_base_url or config.llm_base_url,
|
||||
# LLMConfig uses these as-passed and no longer reads them from global config,
|
||||
# so the caller must forward the Vertex AI settings (mirrors MemoryEngine's
|
||||
# own LLMConfig construction). Without this, provider=vertexai raises
|
||||
# "HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID is required" even when it is set.
|
||||
vertexai_project_id=config.llm_vertexai_project_id,
|
||||
vertexai_region=config.llm_vertexai_region,
|
||||
vertexai_service_account_key=config.llm_vertexai_service_account_key,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from hindsight_api.engine.retain.fact_extraction import (
|
||||
ExtractedFact,
|
||||
ExtractedFactNoCausal,
|
||||
ExtractedFactVerbose,
|
||||
_build_extraction_prompt_and_schema,
|
||||
)
|
||||
|
||||
|
||||
def _baseline_config() -> MagicMock:
|
||||
config = MagicMock()
|
||||
config.entity_labels = None
|
||||
config.entities_allow_free_form = True
|
||||
config.retain_extraction_mode = "concise"
|
||||
config.retain_extract_causal_links = False
|
||||
config.retain_mission = None
|
||||
config.retain_custom_instructions = None
|
||||
config.llm_output_language = None
|
||||
return config
|
||||
|
||||
|
||||
def test_concise_prompt_keeps_user_preferences_rules_and_corrections_world():
|
||||
prompt, _ = _build_extraction_prompt_and_schema(_baseline_config())
|
||||
|
||||
assert '"world": Objective/external facts' in prompt
|
||||
assert "user's preferences, rules, corrections, constraints" in prompt
|
||||
assert 'These stay "world" even when the user states them during an assistant interaction' in prompt
|
||||
assert "Use this for the assistant/agent doing" in prompt
|
||||
assert "not merely for user facts mentioned in conversation" in prompt
|
||||
|
||||
|
||||
def test_fact_type_schema_descriptions_distinguish_user_facts_from_agent_actions():
|
||||
for model in (ExtractedFact, ExtractedFactVerbose, ExtractedFactNoCausal):
|
||||
description = model.model_fields["fact_type"].description
|
||||
|
||||
assert description is not None
|
||||
assert "preferences" in description
|
||||
assert "rules" in description
|
||||
assert "corrections" in description
|
||||
assert "assistant/agent actually performed" in description
|
||||
@@ -30,6 +30,7 @@ def _make_config(llm_max_retries: int = 3, retain_llm_max_retries: int | None =
|
||||
cfg.retain_extraction_mode = "concise"
|
||||
cfg.retain_extract_causal_links = False
|
||||
cfg.retain_mission = None
|
||||
cfg.llm_temperature_retain = 0.1
|
||||
return cfg
|
||||
|
||||
|
||||
@@ -216,3 +217,43 @@ async def test_none_event_date_with_valid_facts_no_crash():
|
||||
|
||||
assert len(facts) == 1
|
||||
assert "Alice visited Paris" in facts[0].fact
|
||||
|
||||
|
||||
def _make_batch_temp_config(temperature):
|
||||
"""Minimal config for _build_request_body temperature tests."""
|
||||
from hindsight_api.config import HindsightConfig
|
||||
|
||||
cfg = MagicMock(spec=HindsightConfig)
|
||||
cfg.llm_temperature_retain = temperature
|
||||
cfg.retain_max_completion_tokens = None
|
||||
cfg.llm_strict_schema = False
|
||||
return cfg
|
||||
|
||||
|
||||
def _make_batch_llm_config():
|
||||
"""Minimal LLMProvider mock for _build_request_body (non-openai skips service_tier)."""
|
||||
from hindsight_api.engine.llm_wrapper import LLMProvider
|
||||
|
||||
llm = MagicMock(spec=LLMProvider)
|
||||
llm.model = "gpt-test"
|
||||
llm.provider = "mock"
|
||||
return llm
|
||||
|
||||
|
||||
def test_build_request_body_forwards_configured_temperature():
|
||||
"""Batch retain path must send the configured retain temperature."""
|
||||
from hindsight_api.engine.retain.fact_extraction import _build_request_body
|
||||
|
||||
body = _build_request_body(_make_batch_llm_config(), _make_batch_temp_config(0.7), "sys", "user", dict)
|
||||
assert body["temperature"] == 0.7
|
||||
|
||||
|
||||
def test_build_request_body_omits_temperature_when_none():
|
||||
"""HINDSIGHT_API_LLM_TEMPERATURE=none must drop temperature from the batch
|
||||
request body too (Azure GPT-5.5 rejects explicit temperatures). Follow-up to
|
||||
#2469, which only de-hardcoded the streaming path and left the batch
|
||||
_build_request_body hardcoding temperature=0.1."""
|
||||
from hindsight_api.engine.retain.fact_extraction import _build_request_body
|
||||
|
||||
body = _build_request_body(_make_batch_llm_config(), _make_batch_temp_config(None), "sys", "user", dict)
|
||||
assert "temperature" not in body
|
||||
|
||||
@@ -165,6 +165,12 @@ async def test_full_api_workflow(api_client, test_bank_id):
|
||||
assert "total_nodes" in stats
|
||||
assert stats["total_nodes"] > 0
|
||||
|
||||
# ?refresh=true forces a fresh recompute, bypassing the cache; same shape.
|
||||
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/stats?refresh=true")
|
||||
assert response.status_code == 200
|
||||
fresh_stats = response.json()
|
||||
assert fresh_stats["total_nodes"] == stats["total_nodes"]
|
||||
|
||||
# Verify bank list returns stats (fact_count, last_document_at)
|
||||
response = await api_client.get("/v1/default/banks")
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"""HTTP + engine integration tests for the knowledge base (folders + pages).
|
||||
|
||||
Pages are seeded directly via the engine (deterministic content, no LLM) so the
|
||||
tree, OKF projection, move/rename, and cascade-delete behaviour can be asserted
|
||||
without consolidation.
|
||||
"""
|
||||
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
import pytest_asyncio
|
||||
|
||||
from hindsight_api.engine.memory_engine import MemoryEngine
|
||||
|
||||
|
||||
def _enc(bank_id: str) -> str:
|
||||
return urllib.parse.quote(bank_id, safe="")
|
||||
|
||||
|
||||
class _Seed:
|
||||
"""Holds the ids created by the seed fixture for assertions."""
|
||||
|
||||
def __init__(self, **ids):
|
||||
self.__dict__.update(ids)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def kb_bank(memory: MemoryEngine, request_context):
|
||||
"""A bank with folders, nested folders, and pages."""
|
||||
bank_id = f"test-kb-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
runbooks = await memory.create_knowledge_folder(bank_id, "Runbooks", request_context=request_context)
|
||||
policies = await memory.create_knowledge_folder(bank_id, "Policies", request_context=request_context)
|
||||
sub = await memory.create_knowledge_folder(
|
||||
bank_id, "Sub", parent_id=runbooks["id"], request_context=request_context
|
||||
)
|
||||
orders = await memory.create_knowledge_page(
|
||||
bank_id,
|
||||
"Orders",
|
||||
"What are the order facts?",
|
||||
"# Orders\n\nOne row per order.",
|
||||
parent_id=runbooks["id"],
|
||||
tags=["type:runbook", "sales", "revenue"],
|
||||
request_context=request_context,
|
||||
)
|
||||
billing = await memory.create_knowledge_page(
|
||||
bank_id,
|
||||
"Billing",
|
||||
"What is the billing policy?",
|
||||
"# Billing\n\nNet-30.",
|
||||
parent_id=policies["id"],
|
||||
tags=["type:policy", "revenue"],
|
||||
request_context=request_context,
|
||||
)
|
||||
loose = await memory.create_knowledge_page(
|
||||
bank_id,
|
||||
"Loose",
|
||||
"A root page.",
|
||||
"# Loose\n\nNo folder, no tags.",
|
||||
tags=[],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
yield (
|
||||
bank_id,
|
||||
_Seed(
|
||||
runbooks=runbooks["id"],
|
||||
policies=policies["id"],
|
||||
sub=sub["id"],
|
||||
orders=orders["id"],
|
||||
billing=billing["id"],
|
||||
loose=loose["id"],
|
||||
orders_mm=orders["mental_model_id"],
|
||||
),
|
||||
)
|
||||
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
class TestTree:
|
||||
async def test_nested_tree(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/tree")
|
||||
assert resp.status_code == 200, resp.text
|
||||
roots = {r["name"]: r for r in resp.json()["roots"]}
|
||||
assert set(roots) == {"Runbooks", "Policies", "Loose"}
|
||||
|
||||
runbooks = roots["Runbooks"]
|
||||
assert runbooks["kind"] == "folder"
|
||||
child_names = {c["name"] for c in runbooks["children"]}
|
||||
assert child_names == {"Sub", "Orders"}
|
||||
|
||||
orders = next(c for c in runbooks["children"] if c["name"] == "Orders")
|
||||
assert orders["kind"] == "page"
|
||||
# Human-created pages are pinned (not curator-managed).
|
||||
assert orders["managed"] is False
|
||||
assert "sales" in orders["tags"]
|
||||
assert roots["Loose"]["kind"] == "page"
|
||||
|
||||
|
||||
class TestPageDefaults:
|
||||
"""A knowledge page is a living document by default: observation-only, delta,
|
||||
auto-refreshing, with a larger token budget than a plain mental model."""
|
||||
|
||||
async def test_default_trigger_and_max_tokens(self, memory: MemoryEngine, request_context):
|
||||
bank_id = f"test-kb-def-{uuid.uuid4().hex[:8]}"
|
||||
page = await memory.create_knowledge_page(
|
||||
bank_id, "P", "What is P?", "seed", request_context=request_context
|
||||
)
|
||||
mm = await memory.get_mental_model(bank_id, page["mental_model_id"], request_context=request_context)
|
||||
assert mm["trigger"] == {
|
||||
"mode": "delta",
|
||||
"fact_types": ["observation"],
|
||||
"exclude_mental_models": True,
|
||||
"refresh_after_consolidation": True,
|
||||
}
|
||||
assert mm["max_tokens"] == 4096
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
async def test_client_trigger_and_max_tokens_override_defaults(self, memory: MemoryEngine, request_context):
|
||||
bank_id = f"test-kb-ovr-{uuid.uuid4().hex[:8]}"
|
||||
page = await memory.create_knowledge_page(
|
||||
bank_id,
|
||||
"P",
|
||||
"What is P?",
|
||||
"seed",
|
||||
trigger={"mode": "full", "refresh_after_consolidation": False},
|
||||
max_tokens=1024,
|
||||
request_context=request_context,
|
||||
)
|
||||
mm = await memory.get_mental_model(bank_id, page["mental_model_id"], request_context=request_context)
|
||||
assert mm["trigger"]["mode"] == "full"
|
||||
assert mm["trigger"].get("refresh_after_consolidation") is False
|
||||
assert mm["max_tokens"] == 1024
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
class TestGetPage:
|
||||
async def test_okf_document(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/pages/{ids.orders}")
|
||||
assert resp.status_code == 200, resp.text
|
||||
page = resp.json()
|
||||
assert page["type"] == "runbook"
|
||||
assert page["body"].startswith("# Orders")
|
||||
assert page["markdown"].startswith("---\n")
|
||||
assert 'type: "runbook"' in page["markdown"]
|
||||
|
||||
async def test_missing_page_404(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/pages/nope")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
class TestCreate:
|
||||
async def test_create_folder(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.post(
|
||||
f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/folders",
|
||||
json={"name": "Guides", "parent_id": None},
|
||||
)
|
||||
assert resp.status_code == 201, resp.text
|
||||
assert resp.json()["kind"] == "folder"
|
||||
assert resp.json()["name"] == "Guides"
|
||||
|
||||
async def test_create_folder_bad_parent(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
# parent that is a page, not a folder → 400
|
||||
resp = await api_client.post(
|
||||
f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/folders",
|
||||
json={"name": "Nope", "parent_id": ids.orders},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
class TestGraphAndExport:
|
||||
async def test_graph_shared_tag_edge(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/graph")
|
||||
assert resp.status_code == 200, resp.text
|
||||
data = resp.json()
|
||||
assert data["total_pages"] == 3
|
||||
# orders & billing share "revenue"; loose has no tags
|
||||
assert data["total_edges"] == 1
|
||||
edge = data["edges"][0]["data"]
|
||||
assert {edge["source"], edge["target"]} == {ids.orders, ids.billing}
|
||||
assert edge["sharedTags"] == ["revenue"]
|
||||
|
||||
async def test_export_bundle_nested_index(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/export")
|
||||
assert resp.status_code == 200, resp.text
|
||||
files = {f["path"]: f["content"] for f in resp.json()["files"]}
|
||||
assert "index.md" in files
|
||||
assert f"{ids.orders}.md" in files
|
||||
# index reflects the folder hierarchy
|
||||
assert "**Runbooks/**" in files["index.md"]
|
||||
assert "One row per order." in files[f"{ids.orders}.md"]
|
||||
|
||||
|
||||
class TestMoveRenameDelete:
|
||||
async def test_rename(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.patch(
|
||||
f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/nodes/{ids.policies}",
|
||||
json={"name": "Compliance"},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert resp.json()["name"] == "Compliance"
|
||||
|
||||
async def test_move_into_folder(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
# move the Loose root page under Policies
|
||||
resp = await api_client.patch(
|
||||
f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/nodes/{ids.loose}",
|
||||
json={"parent_id": ids.policies},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert resp.json()["parent_id"] == ids.policies
|
||||
|
||||
async def test_move_cycle_rejected(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
# moving Runbooks under its own descendant Sub must fail
|
||||
resp = await api_client.patch(
|
||||
f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/nodes/{ids.runbooks}",
|
||||
json={"parent_id": ids.sub},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
async def test_delete_folder_cascades(self, api_client, kb_bank, memory, request_context):
|
||||
bank_id, ids = kb_bank
|
||||
# deleting Runbooks removes Sub + Orders (and Orders' mental model)
|
||||
resp = await api_client.delete(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/nodes/{ids.runbooks}")
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
tree = (await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/tree")).json()
|
||||
root_names = {r["name"] for r in tree["roots"]}
|
||||
assert "Runbooks" not in root_names
|
||||
# the backing mental model is gone too
|
||||
mm = await memory.get_mental_model(bank_id, ids.orders_mm, request_context=request_context)
|
||||
assert mm is None
|
||||
@@ -22,6 +22,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
|
||||
EXTRA_BODY = {"temperature": 0.2, "top_p": 0.9}
|
||||
DEFAULT_HEADERS = {"X-Component-Id": "hindsight", "X-Trace": "abc"}
|
||||
|
||||
|
||||
# ─── config / env parsing ─────────────────────────────────────────────────────
|
||||
@@ -348,7 +349,7 @@ async def test_gemini_cached_parse_retry_keeps_cached_native_schema():
|
||||
# ─── LiteLLM ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _make_litellm_provider(extra_body=None):
|
||||
def _make_litellm_provider(extra_body=None, default_headers=None):
|
||||
pytest.importorskip("litellm")
|
||||
from hindsight_api.engine.providers.litellm_llm import LiteLLMLLM
|
||||
|
||||
@@ -358,6 +359,7 @@ def _make_litellm_provider(extra_body=None):
|
||||
base_url="",
|
||||
model="gpt-4o",
|
||||
extra_body=extra_body,
|
||||
default_headers=default_headers,
|
||||
)
|
||||
|
||||
|
||||
@@ -419,3 +421,107 @@ def test_litellm_router_forwards_extra_body():
|
||||
extra_body=EXTRA_BODY,
|
||||
)
|
||||
assert provider._extra_body == EXTRA_BODY
|
||||
|
||||
|
||||
def test_litellm_stores_default_headers():
|
||||
provider = _make_litellm_provider(default_headers=DEFAULT_HEADERS)
|
||||
assert provider._default_headers == DEFAULT_HEADERS
|
||||
|
||||
|
||||
def test_litellm_empty_default_headers_defaults_to_dict():
|
||||
provider = _make_litellm_provider(default_headers=None)
|
||||
assert provider._default_headers == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_litellm_call_passes_default_headers_as_extra_headers():
|
||||
"""``call()`` forwards default_headers to acompletion via ``extra_headers``."""
|
||||
provider = _make_litellm_provider(default_headers=DEFAULT_HEADERS)
|
||||
provider._acompletion = AsyncMock(return_value=_fake_litellm_response())
|
||||
|
||||
with patch("hindsight_api.engine.providers.litellm_llm.get_metrics_collector"):
|
||||
await provider.call(messages=[{"role": "user", "content": "hi"}], scope="test", max_retries=0)
|
||||
|
||||
assert provider._acompletion.call_args.kwargs.get("extra_headers") == DEFAULT_HEADERS
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_litellm_no_default_headers_omits_extra_headers():
|
||||
"""``call()`` does not pass ``extra_headers`` when none are configured."""
|
||||
provider = _make_litellm_provider(default_headers=None)
|
||||
provider._acompletion = AsyncMock(return_value=_fake_litellm_response())
|
||||
|
||||
with patch("hindsight_api.engine.providers.litellm_llm.get_metrics_collector"):
|
||||
await provider.call(messages=[{"role": "user", "content": "hi"}], scope="test", max_retries=0)
|
||||
|
||||
assert "extra_headers" not in provider._acompletion.call_args.kwargs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_litellm_default_headers_passed_as_fresh_copy():
|
||||
"""Each call gets its own ``extra_headers`` copy so downstream mutation can't
|
||||
contaminate the stored headers or other requests."""
|
||||
provider = _make_litellm_provider(default_headers=DEFAULT_HEADERS)
|
||||
provider._acompletion = AsyncMock(return_value=_fake_litellm_response())
|
||||
|
||||
with patch("hindsight_api.engine.providers.litellm_llm.get_metrics_collector"):
|
||||
await provider.call(messages=[{"role": "user", "content": "hi"}], scope="test", max_retries=0)
|
||||
|
||||
passed = provider._acompletion.call_args.kwargs["extra_headers"]
|
||||
assert passed == DEFAULT_HEADERS
|
||||
assert passed is not provider._default_headers
|
||||
passed["X-Injected"] = "1"
|
||||
assert "X-Injected" not in provider._default_headers
|
||||
|
||||
|
||||
def test_litellm_default_headers_copied_from_caller_dict():
|
||||
"""A caller-owned dict cannot be mutated through the provider."""
|
||||
caller_dict = {"X-Component-Id": "hindsight"}
|
||||
provider = _make_litellm_provider(default_headers=caller_dict)
|
||||
caller_dict["X-Mutated"] = "1"
|
||||
assert "X-Mutated" not in provider._default_headers
|
||||
|
||||
|
||||
def _make_litellm_router_provider(default_headers=None):
|
||||
pytest.importorskip("litellm")
|
||||
from hindsight_api.engine.providers.litellm_router_llm import LiteLLMRouterLLM
|
||||
|
||||
config = {"model_list": [{"model_name": "default", "litellm_params": {"model": "gpt-4o", "api_key": "x"}}]}
|
||||
return LiteLLMRouterLLM(
|
||||
provider="litellmrouter",
|
||||
api_key="",
|
||||
base_url="",
|
||||
model="default",
|
||||
config=config,
|
||||
default_headers=default_headers,
|
||||
)
|
||||
|
||||
|
||||
def test_litellm_router_stores_default_headers():
|
||||
provider = _make_litellm_router_provider(default_headers=DEFAULT_HEADERS)
|
||||
assert provider._default_headers == DEFAULT_HEADERS
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_litellm_router_call_passes_default_headers_as_extra_headers():
|
||||
"""The Router's ``_build_common_kwargs`` override must also forward default_headers
|
||||
as ``extra_headers`` — storage alone doesn't reach the provider behind the Router."""
|
||||
provider = _make_litellm_router_provider(default_headers=DEFAULT_HEADERS)
|
||||
provider._acompletion = AsyncMock(return_value=_fake_litellm_response())
|
||||
|
||||
with patch("hindsight_api.engine.providers.litellm_llm.get_metrics_collector"):
|
||||
await provider.call(messages=[{"role": "user", "content": "hi"}], scope="test", max_retries=0)
|
||||
|
||||
assert provider._acompletion.call_args.kwargs.get("extra_headers") == DEFAULT_HEADERS
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_litellm_router_no_default_headers_omits_extra_headers():
|
||||
"""The Router omits ``extra_headers`` entirely when none are configured."""
|
||||
provider = _make_litellm_router_provider(default_headers=None)
|
||||
provider._acompletion = AsyncMock(return_value=_fake_litellm_response())
|
||||
|
||||
with patch("hindsight_api.engine.providers.litellm_llm.get_metrics_collector"):
|
||||
await provider.call(messages=[{"role": "user", "content": "hi"}], scope="test", max_retries=0)
|
||||
|
||||
assert "extra_headers" not in provider._acompletion.call_args.kwargs
|
||||
|
||||
@@ -45,11 +45,22 @@ def _get_api_key() -> str:
|
||||
|
||||
|
||||
def _make_llm() -> LLMProvider:
|
||||
# LLMProvider uses provider-specific settings as-passed (it does not resolve
|
||||
# them from global config), so forward the ones whose providers require them:
|
||||
# Vertex AI needs project/region, and litellmrouter needs its router config.
|
||||
# Without these, provider=vertexai/litellmrouter raise at construction.
|
||||
from hindsight_api.config import get_config
|
||||
|
||||
config = get_config()
|
||||
return LLMProvider(
|
||||
provider=_PROVIDER,
|
||||
api_key=_get_api_key(),
|
||||
base_url=os.environ.get("HINDSIGHT_API_LLM_BASE_URL", ""),
|
||||
model=_MODEL,
|
||||
vertexai_project_id=config.llm_vertexai_project_id,
|
||||
vertexai_region=config.llm_vertexai_region,
|
||||
vertexai_service_account_key=config.llm_vertexai_service_account_key,
|
||||
litellmrouter_config=config.llm_litellmrouter_config,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -194,6 +194,7 @@ def _make_router_provider(config: dict[str, Any], mock_router: Any) -> LiteLLMRo
|
||||
provider.model = "unused"
|
||||
provider.reasoning_effort = "low"
|
||||
provider.timeout = 300.0
|
||||
provider._default_headers = {}
|
||||
provider.config = config
|
||||
provider._litellm = fake_litellm
|
||||
provider._router = mock_router
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Tests for per-operation LLM temperature configuration from environment variables.
|
||||
|
||||
Covers the resolution order (per-operation env -> global env -> built-in default)
|
||||
and the "omit" sentinels that drop the temperature parameter for models that reject
|
||||
explicit temperatures (e.g. Azure gpt-5.5 -- see issue #2459).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api.config import HindsightConfig, _parse_temperature
|
||||
|
||||
_OP_FIELDS = {
|
||||
"HINDSIGHT_API_LLM_TEMPERATURE_VERIFICATION": ("llm_temperature_verification", 0.0),
|
||||
"HINDSIGHT_API_LLM_TEMPERATURE_RETAIN": ("llm_temperature_retain", 0.1),
|
||||
"HINDSIGHT_API_LLM_TEMPERATURE_REFLECT": ("llm_temperature_reflect", 0.9),
|
||||
"HINDSIGHT_API_LLM_TEMPERATURE_CONSOLIDATION": ("llm_temperature_consolidation", 0.0),
|
||||
}
|
||||
|
||||
|
||||
def _clear_temperature_env(monkeypatch) -> None:
|
||||
monkeypatch.delenv("HINDSIGHT_API_LLM_TEMPERATURE", raising=False)
|
||||
for env_name in _OP_FIELDS:
|
||||
monkeypatch.delenv(env_name, raising=False)
|
||||
|
||||
|
||||
def test_defaults_preserve_historical_values(monkeypatch):
|
||||
_clear_temperature_env(monkeypatch)
|
||||
config = HindsightConfig.from_env()
|
||||
for _, (field, default) in _OP_FIELDS.items():
|
||||
assert getattr(config, field) == default
|
||||
|
||||
|
||||
def test_global_override_applies_to_all_operations(monkeypatch):
|
||||
_clear_temperature_env(monkeypatch)
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_TEMPERATURE", "0.2")
|
||||
config = HindsightConfig.from_env()
|
||||
for _, (field, _default) in _OP_FIELDS.items():
|
||||
assert getattr(config, field) == 0.2
|
||||
|
||||
|
||||
def test_global_none_omits_temperature_everywhere(monkeypatch):
|
||||
_clear_temperature_env(monkeypatch)
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_TEMPERATURE", "none")
|
||||
config = HindsightConfig.from_env()
|
||||
for _, (field, _default) in _OP_FIELDS.items():
|
||||
assert getattr(config, field) is None
|
||||
|
||||
|
||||
def test_per_operation_override_beats_global(monkeypatch):
|
||||
_clear_temperature_env(monkeypatch)
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_TEMPERATURE", "none")
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_TEMPERATURE_RETAIN", "0.5")
|
||||
config = HindsightConfig.from_env()
|
||||
assert config.llm_temperature_retain == 0.5
|
||||
# Other operations still follow the global "none" (omit).
|
||||
assert config.llm_temperature_reflect is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sentinel", ["none", "NONE", "default", "off", "unset", "", " "])
|
||||
def test_omit_sentinels(sentinel):
|
||||
assert _parse_temperature(sentinel) is None
|
||||
|
||||
|
||||
def test_parse_temperature_rejects_out_of_range():
|
||||
with pytest.raises(ValueError):
|
||||
_parse_temperature("2.5")
|
||||
with pytest.raises(ValueError):
|
||||
_parse_temperature("-0.1")
|
||||
|
||||
|
||||
def test_parse_temperature_rejects_non_numeric():
|
||||
with pytest.raises(ValueError):
|
||||
_parse_temperature("warm")
|
||||
@@ -0,0 +1,99 @@
|
||||
"""End-to-end checks that per-operation temperature reaches the LLM call.
|
||||
|
||||
These drive the real pipeline with the mock LLM provider (which records the
|
||||
``temperature`` it receives) and assert that each operation forwards the
|
||||
configured value -- including ``None``, which omits the parameter for models
|
||||
that reject explicit temperatures (issue #2459).
|
||||
|
||||
Config resolution itself is unit-tested in ``test_llm_temperature_env.py``;
|
||||
here we verify the value is actually threaded through to ``provider.call()``.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api.config import clear_config_cache
|
||||
from hindsight_api.engine.search import think_utils
|
||||
|
||||
|
||||
def _calls_for_scope(memory, scope: str) -> list[dict]:
|
||||
"""Collect mock call records for a scope across the engine's LLM configs.
|
||||
|
||||
retain/reflect/consolidation each wrap a distinct provider instance, so a
|
||||
given scope only lands on one of them; gather from all and filter.
|
||||
"""
|
||||
seen_impls: dict[int, object] = {}
|
||||
for config in (
|
||||
memory._llm_config,
|
||||
memory._retain_llm_config,
|
||||
memory._reflect_llm_config,
|
||||
memory._consolidation_llm_config,
|
||||
):
|
||||
impl = config._provider_impl
|
||||
seen_impls[id(impl)] = impl
|
||||
|
||||
calls: list[dict] = []
|
||||
for impl in seen_impls.values():
|
||||
calls.extend(impl.get_mock_calls())
|
||||
return [c for c in calls if c.get("scope") == scope]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retain_forwards_configured_temperature(memory, request_context):
|
||||
"""Retain's fact extraction must call the LLM with the retain temperature (0.1 default)."""
|
||||
bank_id = f"test_temp_retain_{datetime.now(timezone.utc).timestamp()}"
|
||||
try:
|
||||
await memory.retain_async(
|
||||
bank_id=bank_id,
|
||||
content="Alice is a senior engineer at TechCorp. She works on distributed systems.",
|
||||
context="team overview",
|
||||
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc),
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
extract_calls = _calls_for_scope(memory, "retain_extract_facts")
|
||||
assert extract_calls, "retain should have made a fact-extraction LLM call"
|
||||
assert all(c["temperature"] == 0.1 for c in extract_calls)
|
||||
finally:
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reflect_think_forwards_configured_temperature(memory):
|
||||
"""The reflect 'thinking' path must call the LLM with the reflect temperature (0.9 default)."""
|
||||
reflect_config = memory._reflect_llm_config
|
||||
reflect_config._provider_impl.clear_mock_calls()
|
||||
|
||||
await think_utils.reflect(
|
||||
llm_config=reflect_config,
|
||||
query="What does Alice work on?",
|
||||
world_facts=["Alice works on distributed systems."],
|
||||
)
|
||||
|
||||
think_calls = [c for c in reflect_config._provider_impl.get_mock_calls() if c["scope"] == "memory_think"]
|
||||
assert think_calls, "reflect should have made a memory_think LLM call"
|
||||
assert all(c["temperature"] == 0.9 for c in think_calls)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_global_none_omits_temperature_on_real_call(memory, monkeypatch):
|
||||
"""HINDSIGHT_API_LLM_TEMPERATURE=none must omit (None) the temperature on a live call."""
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_TEMPERATURE", "none")
|
||||
clear_config_cache()
|
||||
try:
|
||||
reflect_config = memory._reflect_llm_config
|
||||
reflect_config._provider_impl.clear_mock_calls()
|
||||
|
||||
await think_utils.reflect(
|
||||
llm_config=reflect_config,
|
||||
query="What does Alice work on?",
|
||||
world_facts=["Alice works on distributed systems."],
|
||||
)
|
||||
|
||||
think_calls = [c for c in reflect_config._provider_impl.get_mock_calls() if c["scope"] == "memory_think"]
|
||||
assert think_calls, "reflect should have made a memory_think LLM call"
|
||||
assert all(c["temperature"] is None for c in think_calls), "temperature should be omitted"
|
||||
finally:
|
||||
# Restore the cached config so later tests see default temperatures.
|
||||
clear_config_cache()
|
||||
@@ -0,0 +1,179 @@
|
||||
"""Plumbing tests for the per-operation / global LLM request defaults (issue #2452).
|
||||
|
||||
These assert the *wiring* — that a resolved timeout / retry policy actually reaches
|
||||
the provider that uses it — not just that the env var parses into config (covered by
|
||||
test_config_validation.py).
|
||||
|
||||
The values are threaded
|
||||
``config -> MemoryEngine per-op resolve -> LLMProvider -> (create_llm_provider /
|
||||
call())``. Before the fix the per-operation ``*_llm_timeout`` / ``*_llm_max_retries`` /
|
||||
``*_llm_initial_backoff`` / ``*_llm_max_backoff`` fields (and even the global ``llm_*``)
|
||||
were resolved into ``HindsightConfig`` but never reached the provider, so a configured
|
||||
``HINDSIGHT_API_RETAIN_LLM_TIMEOUT`` silently used the global default
|
||||
(``LiteLLM call exceeded timeout=120.0s``) and the per-op retry knobs were inert.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api.config import DEFAULT_LLM_TIMEOUT
|
||||
from hindsight_api.engine.llm_wrapper import LLMConfig
|
||||
|
||||
|
||||
def _mock_llm(**kwargs) -> LLMConfig:
|
||||
return LLMConfig(provider="mock", api_key="", base_url="", model="m", **kwargs)
|
||||
|
||||
|
||||
def _spy_provider_call(monkeypatch, llm: LLMConfig) -> dict:
|
||||
"""Replace the provider impl's call() with a kwargs-capturing stub."""
|
||||
captured: dict = {}
|
||||
|
||||
async def fake_call(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return "ok"
|
||||
|
||||
monkeypatch.setattr(llm._provider_impl, "call", fake_call)
|
||||
return captured
|
||||
|
||||
|
||||
def test_litellm_provider_impl_receives_timeout():
|
||||
"""LLMConfig -> create_llm_provider -> LiteLLMLLM carries the resolved timeout."""
|
||||
llm = LLMConfig(provider="litellm", api_key="k", base_url="", model="gpt-4o-mini", timeout=300.0)
|
||||
assert llm.timeout == 300.0
|
||||
assert llm._provider_impl.timeout == 300.0
|
||||
|
||||
|
||||
def test_openai_compatible_provider_impl_receives_timeout():
|
||||
"""The OpenAI-compatible path (openai/groq/ollama/...) carries the timeout too."""
|
||||
llm = LLMConfig(provider="openai", api_key="k", base_url="", model="gpt-4o-mini", timeout=250.0)
|
||||
assert llm._provider_impl.timeout == 250.0
|
||||
|
||||
|
||||
def test_timeout_none_falls_back_to_provider_default():
|
||||
"""No timeout passed -> provider falls back to its env/DEFAULT_LLM_TIMEOUT default.
|
||||
|
||||
Guards against a regression where threading the value would override the
|
||||
long-standing default for callers that never configured a timeout.
|
||||
"""
|
||||
llm = LLMConfig(provider="litellm", api_key="k", base_url="", model="gpt-4o-mini")
|
||||
assert llm._provider_impl.timeout == DEFAULT_LLM_TIMEOUT
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _clean_timeout_env(monkeypatch):
|
||||
"""Mock provider + verification off, with all timeout env vars cleared."""
|
||||
from hindsight_api.config import clear_config_cache
|
||||
|
||||
monkeypatch.setenv("HINDSIGHT_API_SKIP_LLM_VERIFICATION", "true")
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_PROVIDER", "mock")
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_MODEL", "default-model")
|
||||
for op in ("LLM", "RETAIN_LLM", "REFLECT_LLM", "CONSOLIDATION_LLM"):
|
||||
for knob in ("TIMEOUT", "MAX_RETRIES", "INITIAL_BACKOFF", "MAX_BACKOFF"):
|
||||
monkeypatch.delenv(f"HINDSIGHT_API_{op}_{knob}", raising=False)
|
||||
clear_config_cache()
|
||||
yield
|
||||
clear_config_cache()
|
||||
|
||||
|
||||
async def test_call_uses_instance_retry_defaults(monkeypatch):
|
||||
"""call() falls back to the provider's configured retry policy when no per-call
|
||||
arg is given — this is what makes a per-op ``*_llm_max_retries`` take effect."""
|
||||
llm = _mock_llm(max_retries=7, initial_backoff=2.0, max_backoff=9.0)
|
||||
captured = _spy_provider_call(monkeypatch, llm)
|
||||
|
||||
await llm.call(messages=[{"role": "user", "content": "hi"}], scope="x")
|
||||
|
||||
assert captured["max_retries"] == 7
|
||||
assert captured["initial_backoff"] == 2.0
|
||||
assert captured["max_backoff"] == 9.0
|
||||
|
||||
|
||||
async def test_call_explicit_arg_overrides_instance_default(monkeypatch):
|
||||
"""An explicit per-call value still wins over the configured default."""
|
||||
llm = _mock_llm(max_retries=7)
|
||||
captured = _spy_provider_call(monkeypatch, llm)
|
||||
|
||||
await llm.call(messages=[{"role": "user", "content": "hi"}], scope="x", max_retries=2)
|
||||
|
||||
assert captured["max_retries"] == 2
|
||||
|
||||
|
||||
async def test_call_falls_back_to_method_default_when_unconfigured(monkeypatch):
|
||||
"""No instance config and no per-call arg -> the method's own fallback (10),
|
||||
so providers built outside MemoryEngine (from_env, tests) are unchanged."""
|
||||
llm = _mock_llm()
|
||||
captured = _spy_provider_call(monkeypatch, llm)
|
||||
|
||||
await llm.call(messages=[{"role": "user", "content": "hi"}], scope="x")
|
||||
|
||||
assert captured["max_retries"] == 10
|
||||
assert captured["initial_backoff"] == 1.0
|
||||
assert captured["max_backoff"] == 60.0
|
||||
|
||||
|
||||
def test_memory_engine_threads_per_operation_timeout(monkeypatch, _clean_timeout_env):
|
||||
"""Each per-operation override reaches its own LLM config; the rest fall back
|
||||
to the global ``llm_timeout``."""
|
||||
from hindsight_api import MemoryEngine
|
||||
from hindsight_api.config import clear_config_cache
|
||||
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_TIMEOUT", "100")
|
||||
monkeypatch.setenv("HINDSIGHT_API_RETAIN_LLM_TIMEOUT", "300")
|
||||
monkeypatch.setenv("HINDSIGHT_API_CONSOLIDATION_LLM_TIMEOUT", "450")
|
||||
# reflect intentionally unset -> inherits the global 100
|
||||
clear_config_cache()
|
||||
|
||||
engine = MemoryEngine(skip_llm_verification=True)
|
||||
|
||||
assert engine._llm_config.timeout == 100.0
|
||||
assert engine._retain_llm_config.timeout == 300.0
|
||||
assert engine._reflect_llm_config.timeout == 100.0
|
||||
assert engine._consolidation_llm_config.timeout == 450.0
|
||||
|
||||
|
||||
def test_memory_engine_threads_per_operation_retry_policy(monkeypatch, _clean_timeout_env):
|
||||
"""Per-op retry/backoff overrides reach their own config; unset ops fall back
|
||||
to the global ``llm_max_retries`` / ``llm_initial_backoff`` / ``llm_max_backoff``."""
|
||||
from hindsight_api import MemoryEngine
|
||||
from hindsight_api.config import clear_config_cache
|
||||
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_MAX_RETRIES", "4")
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_INITIAL_BACKOFF", "0.5")
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_MAX_BACKOFF", "20")
|
||||
monkeypatch.setenv("HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES", "2")
|
||||
monkeypatch.setenv("HINDSIGHT_API_CONSOLIDATION_LLM_MAX_BACKOFF", "99")
|
||||
clear_config_cache()
|
||||
|
||||
engine = MemoryEngine(skip_llm_verification=True)
|
||||
|
||||
# Global applies everywhere unless overridden.
|
||||
assert engine._llm_config.max_retries == 4
|
||||
assert engine._retain_llm_config.max_retries == 4
|
||||
# reflect overrides only max_retries; backoff inherits global.
|
||||
assert engine._reflect_llm_config.max_retries == 2
|
||||
assert engine._reflect_llm_config.initial_backoff == 0.5
|
||||
# consolidation overrides only max_backoff; retries inherit global.
|
||||
assert engine._consolidation_llm_config.max_retries == 4
|
||||
assert engine._consolidation_llm_config.max_backoff == 99.0
|
||||
|
||||
|
||||
def test_memory_engine_per_op_defaults_to_global_default(_clean_timeout_env):
|
||||
"""With nothing configured, every operation uses the documented global defaults."""
|
||||
from hindsight_api import MemoryEngine
|
||||
from hindsight_api.config import (
|
||||
DEFAULT_LLM_INITIAL_BACKOFF,
|
||||
DEFAULT_LLM_MAX_BACKOFF,
|
||||
DEFAULT_LLM_MAX_RETRIES,
|
||||
)
|
||||
|
||||
engine = MemoryEngine(skip_llm_verification=True)
|
||||
|
||||
for cfg in (
|
||||
engine._llm_config,
|
||||
engine._retain_llm_config,
|
||||
engine._reflect_llm_config,
|
||||
engine._consolidation_llm_config,
|
||||
):
|
||||
assert cfg.timeout == DEFAULT_LLM_TIMEOUT
|
||||
assert cfg.max_retries == DEFAULT_LLM_MAX_RETRIES
|
||||
assert cfg.initial_backoff == DEFAULT_LLM_INITIAL_BACKOFF
|
||||
assert cfg.max_backoff == DEFAULT_LLM_MAX_BACKOFF
|
||||
@@ -389,6 +389,53 @@ async def test_retain_extract_success_records_usage_once(registered_recorder):
|
||||
assert r.cached_tokens == 20
|
||||
|
||||
|
||||
# ── real provider: litellm tool-call arg-parse failure keeps usage (#2387) ────
|
||||
|
||||
|
||||
def _litellm_tool_response_with_usage(arguments: str):
|
||||
"""A successful LiteLLM (OpenAI-shaped) tool-call response carrying usage,
|
||||
like ``call_with_tools`` sees right before it ``json.loads`` the tool
|
||||
arguments."""
|
||||
function = SimpleNamespace(name="extract", arguments=arguments)
|
||||
tool_call = SimpleNamespace(id="call_1", function=function)
|
||||
message = SimpleNamespace(content=None, tool_calls=[tool_call])
|
||||
choice = SimpleNamespace(finish_reason="tool_calls", message=message)
|
||||
usage = SimpleNamespace(
|
||||
prompt_tokens=140,
|
||||
completion_tokens=18,
|
||||
total_tokens=158,
|
||||
prompt_tokens_details=SimpleNamespace(cached_tokens=20),
|
||||
)
|
||||
return SimpleNamespace(error=None, usage=usage, choices=[choice])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_litellm_tool_call_arg_parse_failure_keeps_usage(registered_recorder):
|
||||
"""The litellm tool path bills the provider response, then ``json.loads`` the
|
||||
tool-call arguments locally; malformed args raise after billing, so the error
|
||||
trace must keep the provider-reported tokens. Exercises the real
|
||||
``LiteLLMLLM.call_with_tools`` stash that ``LiteLLMRouterLLM`` also inherits
|
||||
(the wrapper-level tools test uses a provider that already stashes)."""
|
||||
llm = LLMProvider(provider="litellm", api_key="test-key", base_url="https://example.test/v1", model="gpt-4o-mini")
|
||||
# Valid response + usage, but the tool arguments are not valid JSON.
|
||||
llm._provider_impl._acompletion = AsyncMock(return_value=_litellm_tool_response_with_usage("{not valid json"))
|
||||
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
await llm.call_with_tools(
|
||||
messages=[{"role": "user", "content": "x"}],
|
||||
tools=[],
|
||||
scope="tools",
|
||||
max_retries=0,
|
||||
)
|
||||
|
||||
assert len(registered_recorder.records) == 1
|
||||
r = registered_recorder.records[0]
|
||||
assert r.status == "error"
|
||||
assert r.input_tokens == 140
|
||||
assert r.output_tokens == 18
|
||||
assert r.cached_tokens == 20
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_configured_provider_binds_bank_context(registered_recorder):
|
||||
llm = LLMProvider(provider="mock", api_key="", base_url="", model="mock")
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Unit tests for the markitdown file parser."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api.engine.parsers.markitdown import MarkitdownParser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser() -> MarkitdownParser:
|
||||
return MarkitdownParser()
|
||||
|
||||
|
||||
def _utf8_json_with_ascii_prefix() -> bytes:
|
||||
"""A JSON file whose first chunk is ASCII but contains multibyte UTF-8 later.
|
||||
|
||||
markitdown samples only the first chunk for charset detection, so this layout
|
||||
used to be mis-detected as ASCII and crash the JSON/ipynb converter on the
|
||||
first multibyte byte (0xc3).
|
||||
"""
|
||||
payload = {"messages": [{"role": "user", "text": "x" * 6400 + " café à la crème naïve über"}]}
|
||||
raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
assert any(b > 127 for b in raw[6000:]), "fixture must have non-ASCII bytes past the sample window"
|
||||
return raw
|
||||
|
||||
|
||||
async def test_convert_utf8_json_transcript(parser: MarkitdownParser):
|
||||
"""A UTF-8 JSON transcript with an ASCII prefix parses without a decode error."""
|
||||
file_data = _utf8_json_with_ascii_prefix()
|
||||
|
||||
content = await parser.convert(file_data, "transcript.json")
|
||||
|
||||
assert "café" in content
|
||||
assert "crème" in content
|
||||
|
||||
|
||||
async def test_convert_plain_utf8_text(parser: MarkitdownParser):
|
||||
"""A plain UTF-8 text file with non-ASCII content round-trips."""
|
||||
file_data = ("über résumé\n" + "a" * 7000 + "\nfin: naïveté").encode("utf-8")
|
||||
|
||||
content = await parser.convert(file_data, "notes.txt")
|
||||
|
||||
assert "über" in content
|
||||
assert "naïveté" in content
|
||||
|
||||
|
||||
def test_utf8_stream_info_for_text_extension():
|
||||
"""Text files that are valid UTF-8 get an explicit UTF-8 charset hint."""
|
||||
info = MarkitdownParser._utf8_stream_info("über".encode("utf-8"), "a.json")
|
||||
|
||||
assert info is not None
|
||||
assert info.charset == "utf-8"
|
||||
|
||||
|
||||
def test_utf8_stream_info_skips_binary_extension():
|
||||
"""Binary files are left to markitdown's own detection (no hint)."""
|
||||
assert MarkitdownParser._utf8_stream_info(b"%PDF-1.4 ...", "a.pdf") is None
|
||||
|
||||
|
||||
def test_utf8_stream_info_skips_non_utf8_text():
|
||||
"""Non-UTF-8 text falls back to markitdown's detection (no hint)."""
|
||||
latin1 = "café".encode("latin-1") # 0xe9, invalid as standalone UTF-8
|
||||
|
||||
assert MarkitdownParser._utf8_stream_info(latin1, "a.txt") is None
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Regression for the consolidator search_vector gap (PR #2425).
|
||||
|
||||
Observations written by the consolidator under the ``native`` text-search
|
||||
backend landed with a NULL ``search_vector`` and were invisible to BM25. The
|
||||
writer is fixed to populate the tsvector; migration
|
||||
``c3f7a1b9d2e4`` backfills the historical NULL observations.
|
||||
|
||||
This test seeds an observation with a NULL ``search_vector`` at the revision
|
||||
just before the backfill, runs the migration to head, and asserts the row is
|
||||
populated with a valid tsvector — and that already-populated rows are left
|
||||
untouched. Uses a dedicated pg0 instance (mirrors test_migration_backsweep) so
|
||||
it controls exactly which migrations have run and never stamps the shared test
|
||||
instance.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
_SCRIPT_LOCATION = str(Path(__file__).parent.parent / "hindsight_api" / "alembic")
|
||||
|
||||
# Revision immediately before the backfill migration.
|
||||
_PRE_BACKFILL_REVISION = "f4d1c2b3a5e6"
|
||||
_BACKFILL_REVISION = "c3f7a1b9d2e4"
|
||||
|
||||
|
||||
def _alembic_cfg(db_url: str) -> Config:
|
||||
cfg = Config()
|
||||
cfg.set_main_option("script_location", _SCRIPT_LOCATION)
|
||||
cfg.set_main_option("sqlalchemy.url", db_url)
|
||||
cfg.set_main_option("prepend_sys_path", ".")
|
||||
cfg.set_main_option("path_separator", "os")
|
||||
return cfg
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def pre_backfill_db_url():
|
||||
"""pg0 instance brought to the revision just before the backfill so the
|
||||
migration's UPDATE runs against seeded NULL-search_vector observations."""
|
||||
from hindsight_api.pg0 import EmbeddedPostgres
|
||||
|
||||
pg0 = EmbeddedPostgres(name="hindsight-obs-sv-backfill-test", port=5568)
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
url = loop.run_until_complete(pg0.ensure_running())
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
# pg0 data dirs persist across runs, so normalise: go to head, then down to
|
||||
# just before the backfill.
|
||||
command.upgrade(_alembic_cfg(url), "heads")
|
||||
command.downgrade(_alembic_cfg(url), _PRE_BACKFILL_REVISION)
|
||||
return url
|
||||
|
||||
|
||||
def test_backfill_populates_null_observation_search_vector(pre_backfill_db_url):
|
||||
db_url = pre_backfill_db_url
|
||||
bank_id = f"obs-sv-{uuid.uuid4().hex[:12]}"
|
||||
|
||||
null_obs_id = uuid.uuid4()
|
||||
populated_obs_id = uuid.uuid4()
|
||||
world_id = uuid.uuid4()
|
||||
|
||||
engine = create_engine(db_url)
|
||||
with engine.connect() as conn:
|
||||
# Sanity: under the default native backend search_vector is a regular
|
||||
# tsvector column; otherwise this test wouldn't exercise the gate.
|
||||
udt = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT udt_name FROM information_schema.columns
|
||||
WHERE table_name = 'memory_units' AND column_name = 'search_vector'
|
||||
"""
|
||||
)
|
||||
).scalar()
|
||||
assert udt == "tsvector", f"expected native tsvector backend, got {udt!r}"
|
||||
|
||||
conn.execute(text("INSERT INTO banks (bank_id) VALUES (:b)"), {"b": bank_id})
|
||||
|
||||
# The bug shape: an observation with NULL search_vector.
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO memory_units (id, bank_id, text, fact_type, search_vector)
|
||||
VALUES (:id, :b, 'Django uses middleware for request processing', 'observation', NULL)
|
||||
"""
|
||||
),
|
||||
{"id": null_obs_id, "b": bank_id},
|
||||
)
|
||||
# An observation already populated — must be left byte-for-byte intact.
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO memory_units (id, bank_id, text, fact_type, search_vector)
|
||||
VALUES (:id, :b, 'Already indexed observation', 'observation',
|
||||
to_tsvector('english', 'Already indexed observation'))
|
||||
"""
|
||||
),
|
||||
{"id": populated_obs_id, "b": bank_id},
|
||||
)
|
||||
# A non-observation row with NULL search_vector — must NOT be touched
|
||||
# (the migration is scoped to fact_type = 'observation').
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO memory_units (id, bank_id, text, fact_type, search_vector)
|
||||
VALUES (:id, :b, 'A world fact', 'world', NULL)
|
||||
"""
|
||||
),
|
||||
{"id": world_id, "b": bank_id},
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
# Run the backfill.
|
||||
command.upgrade(_alembic_cfg(db_url), _BACKFILL_REVISION)
|
||||
|
||||
with engine.connect() as conn:
|
||||
null_obs_sv, null_obs_match = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT search_vector IS NOT NULL,
|
||||
search_vector @@ plainto_tsquery('english', 'middleware')
|
||||
FROM memory_units WHERE id = :id
|
||||
"""
|
||||
),
|
||||
{"id": null_obs_id},
|
||||
).fetchone()
|
||||
assert null_obs_sv, "backfill must populate the NULL observation's search_vector"
|
||||
assert null_obs_match, "backfilled tsvector must be BM25-searchable on its own text"
|
||||
|
||||
populated_match = conn.execute(
|
||||
text("SELECT search_vector @@ plainto_tsquery('english', 'indexed') FROM memory_units WHERE id = :id"),
|
||||
{"id": populated_obs_id},
|
||||
).scalar()
|
||||
assert populated_match, "pre-populated observation must remain searchable"
|
||||
|
||||
world_null = conn.execute(
|
||||
text("SELECT search_vector IS NULL FROM memory_units WHERE id = :id"),
|
||||
{"id": world_id},
|
||||
).scalar()
|
||||
assert world_null, "non-observation rows must be left untouched by the backfill"
|
||||
|
||||
# Idempotency: re-running touches nothing and stays at head.
|
||||
command.upgrade(_alembic_cfg(db_url), "heads")
|
||||
with engine.connect() as conn:
|
||||
still_populated = conn.execute(
|
||||
text("SELECT search_vector IS NOT NULL FROM memory_units WHERE id = :id"),
|
||||
{"id": null_obs_id},
|
||||
).scalar()
|
||||
assert still_populated
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Startup/lazy model-init must fail fast instead of hanging forever.
|
||||
Startup model-init must fail fast instead of hanging forever.
|
||||
|
||||
Covers issue #1897: if a model load blocks (e.g. an offline HuggingFace
|
||||
download or an unreachable provider), initialization is capped by a wall-clock
|
||||
@@ -68,7 +68,7 @@ def test_default_model_init_timeout_is_300s():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lazy_reranker_init_fails_fast_on_hang():
|
||||
async def test_reranker_ensure_initialized_fails_fast_on_hang():
|
||||
"""A stuck cross-encoder load raises RuntimeError within the timeout, not forever."""
|
||||
reranker = CrossEncoderReranker(cross_encoder=_HangingCrossEncoder())
|
||||
config = _make_config(model_init_timeout=0.1)
|
||||
@@ -81,7 +81,7 @@ async def test_lazy_reranker_init_fails_fast_on_hang():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lazy_reranker_init_succeeds_within_timeout():
|
||||
async def test_reranker_ensure_initialized_succeeds_within_timeout():
|
||||
"""A fast init completes normally and marks the reranker initialized."""
|
||||
encoder = _FastCrossEncoder()
|
||||
reranker = CrossEncoderReranker(cross_encoder=encoder)
|
||||
|
||||
@@ -70,7 +70,6 @@ async def test_global_default_dtype_restored_to_float32_after_init():
|
||||
query_analyzer=_NoopQueryAnalyzer(),
|
||||
run_migrations=False,
|
||||
skip_llm_verification=True,
|
||||
lazy_reranker=False, # load the cross-encoder eagerly, in the gather
|
||||
task_backend=SyncTaskBackend(),
|
||||
)
|
||||
|
||||
|
||||
@@ -15,9 +15,13 @@ from hindsight_api.config import (
|
||||
_parse_llm_strategy,
|
||||
)
|
||||
from hindsight_api.engine.llm_wrapper import LLMProvider
|
||||
from hindsight_api.engine.memory_engine import _build_llm
|
||||
from hindsight_api.engine.memory_engine import _build_llm, _LLMCallDefaults
|
||||
from hindsight_api.engine.multi_llm import MultiLLMProvider
|
||||
|
||||
# No per-request overrides — exercises the chain-resolution logic without
|
||||
# touching timeout/retry defaults (those have their own tests).
|
||||
_NO_CALL_DEFAULTS = _LLMCallDefaults(timeout=None, max_retries=None, initial_backoff=None, max_backoff=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clean_llm_env(monkeypatch):
|
||||
@@ -248,7 +252,7 @@ def _base_llm() -> LLMProvider:
|
||||
|
||||
def test_build_llm_no_chain_returns_plain_provider(clean_llm_env):
|
||||
base = _base_llm()
|
||||
result = _build_llm(base, _empty_config(), "")
|
||||
result = _build_llm(base, _empty_config(), "", _NO_CALL_DEFAULTS)
|
||||
assert result is base
|
||||
assert not isinstance(result, MultiLLMProvider)
|
||||
|
||||
@@ -259,7 +263,7 @@ def test_build_llm_global_chain_wraps(clean_llm_env):
|
||||
llm_strategy=LLMStrategyConfig(mode="failover"),
|
||||
)
|
||||
base = _base_llm()
|
||||
result = _build_llm(base, config, "")
|
||||
result = _build_llm(base, config, "", _NO_CALL_DEFAULTS)
|
||||
assert isinstance(result, MultiLLMProvider)
|
||||
assert result.members[0] is base # primary stays index 0
|
||||
assert result.members[1].provider == "ollama"
|
||||
@@ -272,7 +276,7 @@ def test_build_llm_per_op_inherits_global(clean_llm_env):
|
||||
retain_llm_members=[],
|
||||
retain_llm_strategy=None,
|
||||
)
|
||||
result = _build_llm(_base_llm(), config, "retain_")
|
||||
result = _build_llm(_base_llm(), config, "retain_", _NO_CALL_DEFAULTS)
|
||||
assert isinstance(result, MultiLLMProvider)
|
||||
assert [m.provider for m in result.members[1:]] == ["ollama"] # inherited
|
||||
|
||||
@@ -284,7 +288,7 @@ def test_build_llm_per_op_overrides_global(clean_llm_env):
|
||||
retain_llm_members=[_member("lmstudio")],
|
||||
retain_llm_strategy=LLMStrategyConfig(mode="round-robin"),
|
||||
)
|
||||
result = _build_llm(_base_llm(), config, "retain_")
|
||||
result = _build_llm(_base_llm(), config, "retain_", _NO_CALL_DEFAULTS)
|
||||
assert isinstance(result, MultiLLMProvider)
|
||||
assert [m.provider for m in result.members[1:]] == ["lmstudio"]
|
||||
assert result._strategy.mode == "round-robin"
|
||||
@@ -294,7 +298,7 @@ def test_build_llm_members_without_strategy_stays_plain(clean_llm_env):
|
||||
# Members configured but no strategy → no wrapping (strategy is required).
|
||||
config = _empty_config(llm_members=[_member("ollama")], llm_strategy=None)
|
||||
base = _base_llm()
|
||||
assert _build_llm(base, config, "") is base
|
||||
assert _build_llm(base, config, "", _NO_CALL_DEFAULTS) is base
|
||||
|
||||
|
||||
# ── vertexai member build path (_member_to_llm) ─────────────────────────────────
|
||||
@@ -333,7 +337,7 @@ def test_member_to_llm_passes_vertexai_project_and_region(clean_llm_env, monkeyp
|
||||
vertexai_project_id="member-proj",
|
||||
vertexai_region="europe-west1",
|
||||
)
|
||||
provider = _member_to_llm(member, _empty_config())
|
||||
provider = _member_to_llm(member, _empty_config(), _NO_CALL_DEFAULTS)
|
||||
|
||||
assert provider.provider == "vertexai"
|
||||
# Project/region flowed all the way to the Vertex AI SDK client.
|
||||
@@ -386,7 +390,7 @@ def test_member_to_llm_passes_vertexai_service_account_key(clean_llm_env, monkey
|
||||
vertexai_project_id="member-proj",
|
||||
vertexai_service_account_key="/keys/member-sa.json",
|
||||
)
|
||||
provider = _member_to_llm(member, _empty_config())
|
||||
provider = _member_to_llm(member, _empty_config(), _NO_CALL_DEFAULTS)
|
||||
|
||||
assert provider.provider == "vertexai"
|
||||
# The member's key path was loaded, and the credentials reached the SDK client.
|
||||
@@ -432,7 +436,7 @@ def test_member_to_llm_passes_litellmrouter_config(clean_llm_env, monkeypatch):
|
||||
gemini_service_tier=None,
|
||||
litellmrouter_config=router_cfg,
|
||||
)
|
||||
provider = _member_to_llm(member, _empty_config())
|
||||
provider = _member_to_llm(member, _empty_config(), _NO_CALL_DEFAULTS)
|
||||
|
||||
assert provider.provider == "litellmrouter"
|
||||
# The member's own router config flowed to the LiteLLM router build.
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Pure unit tests for the OKF (Open Knowledge Format) serializer.
|
||||
|
||||
These exercise hindsight_api/api/okf.py with plain dicts — no DB, no LLM — so
|
||||
they pin the OKF contract (frontmatter projection, type-from-tag, shared-tag
|
||||
graph) deterministically and fast.
|
||||
"""
|
||||
|
||||
from hindsight_api.api import okf
|
||||
|
||||
|
||||
def _mm(**overrides):
|
||||
base = {
|
||||
"id": "orders",
|
||||
"name": "Orders",
|
||||
"source_query": "What are the order facts?",
|
||||
"content": "# Orders\n\nOne row per order.",
|
||||
"tags": ["type:runbook", "sales", "revenue"],
|
||||
"last_refreshed_at": "2026-01-02T00:00:00Z",
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
class TestPageType:
|
||||
def test_lifts_type_from_tag_and_drops_it(self):
|
||||
pt = okf.page_type(["type:runbook", "sales", "revenue"])
|
||||
assert pt.type == "runbook"
|
||||
assert pt.display_tags == ["sales", "revenue"]
|
||||
|
||||
def test_defaults_when_no_type_tag(self):
|
||||
pt = okf.page_type(["sales"])
|
||||
assert pt.type == okf.DEFAULT_PAGE_TYPE
|
||||
assert pt.display_tags == ["sales"]
|
||||
|
||||
def test_handles_none_and_empty(self):
|
||||
assert okf.page_type(None).type == okf.DEFAULT_PAGE_TYPE
|
||||
assert okf.page_type(None).display_tags == []
|
||||
|
||||
def test_blank_type_suffix_falls_back(self):
|
||||
pt = okf.page_type(["type:", "sales"])
|
||||
assert pt.type == okf.DEFAULT_PAGE_TYPE
|
||||
# the (blank) type tag is still stripped from display tags
|
||||
assert pt.display_tags == ["sales"]
|
||||
|
||||
def test_first_type_tag_wins(self):
|
||||
pt = okf.page_type(["type:runbook", "type:guide"])
|
||||
assert pt.type == "runbook"
|
||||
assert pt.display_tags == []
|
||||
|
||||
|
||||
class TestFrontmatter:
|
||||
def test_projects_expected_fields(self):
|
||||
fm = okf.frontmatter(_mm())
|
||||
assert fm["id"] == "orders"
|
||||
assert fm["type"] == "runbook"
|
||||
assert fm["title"] == "Orders"
|
||||
assert fm["description"] == "What are the order facts?"
|
||||
assert fm["tags"] == ["sales", "revenue"]
|
||||
assert fm["timestamp"] == "2026-01-02T00:00:00Z"
|
||||
|
||||
def test_timestamp_falls_back_to_created_at(self):
|
||||
fm = okf.frontmatter(_mm(last_refreshed_at=None))
|
||||
assert fm["timestamp"] == "2026-01-01T00:00:00Z"
|
||||
|
||||
def test_render_omits_none_and_empty(self):
|
||||
rendered = okf.render_frontmatter({"type": "x", "title": None, "tags": []})
|
||||
assert "title" not in rendered
|
||||
assert "tags" not in rendered
|
||||
assert 'type: "x"' in rendered
|
||||
|
||||
def test_render_quotes_and_escapes(self):
|
||||
# A name that looks like a YAML bool / contains a quote must stay a string.
|
||||
rendered = okf.render_frontmatter({"title": 'true "x"'})
|
||||
assert 'title: "true \\"x\\""' in rendered
|
||||
|
||||
|
||||
class TestRenderDocument:
|
||||
def test_includes_frontmatter_and_body(self):
|
||||
doc = okf.render_document(_mm())
|
||||
assert doc.startswith("---\n")
|
||||
assert 'type: "runbook"' in doc
|
||||
assert "One row per order." in doc
|
||||
|
||||
def test_empty_body(self):
|
||||
doc = okf.render_document(_mm(content=""))
|
||||
assert doc.count("---") == 2
|
||||
assert doc.rstrip().endswith("---")
|
||||
|
||||
|
||||
class TestKnowledgeGraph:
|
||||
def test_edge_from_shared_tag(self):
|
||||
pages = [
|
||||
_mm(id="orders", tags=["type:runbook", "sales", "revenue"]),
|
||||
_mm(id="customers", tags=["sales", "crm"]),
|
||||
_mm(id="lonely", tags=[]),
|
||||
]
|
||||
graph = okf.knowledge_graph(pages)
|
||||
assert len(graph.nodes) == 3
|
||||
assert len(graph.edges) == 1
|
||||
edge = graph.edges[0]["data"]
|
||||
assert {edge["source"], edge["target"]} == {"orders", "customers"}
|
||||
assert edge["sharedTags"] == ["sales"]
|
||||
assert edge["weight"] == 1
|
||||
|
||||
def test_type_tag_does_not_create_edges(self):
|
||||
# Two pages sharing only a type: tag must NOT be linked.
|
||||
pages = [
|
||||
_mm(id="a", tags=["type:runbook"]),
|
||||
_mm(id="b", tags=["type:runbook"]),
|
||||
]
|
||||
graph = okf.knowledge_graph(pages)
|
||||
assert graph.edges == []
|
||||
|
||||
def test_node_carries_type_and_color(self):
|
||||
graph = okf.knowledge_graph([_mm(id="orders", tags=["type:runbook", "sales"])])
|
||||
node = graph.nodes[0]["data"]
|
||||
assert node["type"] == "runbook"
|
||||
assert node["label"] == "Orders"
|
||||
assert node["tagCount"] == 1
|
||||
assert node["color"].startswith("#")
|
||||
|
||||
def test_weight_counts_shared_tags(self):
|
||||
pages = [
|
||||
_mm(id="a", tags=["sales", "revenue", "x"]),
|
||||
_mm(id="b", tags=["sales", "revenue", "y"]),
|
||||
]
|
||||
graph = okf.knowledge_graph(pages)
|
||||
assert graph.edges[0]["data"]["weight"] == 2
|
||||
assert graph.edges[0]["data"]["sharedTags"] == ["revenue", "sales"]
|
||||
|
||||
|
||||
class TestReservedFiles:
|
||||
def test_index_links_each_page(self):
|
||||
index = okf.render_index([_mm(id="orders", name="Orders", source_query="q?")])
|
||||
assert "[Orders](./orders.md)" in index
|
||||
assert "q?" in index
|
||||
assert 'type: "index"' in index
|
||||
|
||||
def test_index_empty(self):
|
||||
assert "No knowledge pages yet" in okf.render_index([])
|
||||
|
||||
def test_index_nests_folders(self):
|
||||
nodes = [
|
||||
{"id": "f1", "kind": "folder", "name": "Runbooks", "parent_id": None},
|
||||
{"id": "p1", "kind": "page", "name": "Orders", "parent_id": "f1", "source_query": "q?"},
|
||||
{"id": "p2", "kind": "page", "name": "Loose", "parent_id": None},
|
||||
]
|
||||
idx = okf.render_index(nodes)
|
||||
assert "**Runbooks/**" in idx
|
||||
# the page nested in the folder is indented and links to its file
|
||||
assert " - [Orders](./p1.md) — q?" in idx
|
||||
assert "- [Loose](./p2.md)" in idx
|
||||
|
||||
def test_log_renders_history_newest_first(self):
|
||||
history = [
|
||||
{"previous_content": "v2", "changed_at": "2026-01-02T00:00:00Z"},
|
||||
{"previous_content": "v1", "changed_at": "2026-01-01T00:00:00Z"},
|
||||
]
|
||||
log = okf.render_log(_mm(), history)
|
||||
assert 'type: "log"' in log
|
||||
assert log.index("2026-01-02") < log.index("2026-01-01")
|
||||
assert "v2" in log and "v1" in log
|
||||
|
||||
def test_log_empty(self):
|
||||
assert "No refresh history" in okf.render_log(_mm(), [])
|
||||
@@ -17,7 +17,6 @@ def setup_test_env():
|
||||
# Save original environment values
|
||||
env_vars_to_set = {
|
||||
"HINDSIGHT_API_SKIP_LLM_VERIFICATION": "true",
|
||||
"HINDSIGHT_API_LAZY_RERANKER": "true",
|
||||
"HINDSIGHT_API_LLM_PROVIDER": "mock",
|
||||
"HINDSIGHT_API_LLM_MODEL": "default-model",
|
||||
"HINDSIGHT_API_RETAIN_LLM_PROVIDER": "mock",
|
||||
@@ -76,7 +75,6 @@ class TestPerOperationLLMConfig:
|
||||
|
||||
engine = MemoryEngine(
|
||||
skip_llm_verification=True,
|
||||
lazy_reranker=True,
|
||||
)
|
||||
|
||||
# Verify default config
|
||||
@@ -91,6 +89,33 @@ class TestPerOperationLLMConfig:
|
||||
assert engine._reflect_llm_config.provider == "mock"
|
||||
assert engine._reflect_llm_config.model == "reflect-model"
|
||||
|
||||
def test_groq_openai_service_tier_threaded_into_per_operation_configs(self, monkeypatch):
|
||||
"""The groq/openai service-tier config knobs must reach every per-operation
|
||||
LLM config, like bedrock/gemini already do. Previously they were parsed into
|
||||
HindsightConfig but never threaded into the constructed providers, so setting
|
||||
them was a silent no-op (groq is the default provider)."""
|
||||
from hindsight_api import MemoryEngine
|
||||
from hindsight_api.config import clear_config_cache
|
||||
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_GROQ_SERVICE_TIER", "flex")
|
||||
monkeypatch.setenv("HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER", "flex")
|
||||
clear_config_cache()
|
||||
|
||||
engine = MemoryEngine(
|
||||
skip_llm_verification=True,
|
||||
)
|
||||
|
||||
for cfg in (
|
||||
engine._llm_config,
|
||||
engine._retain_llm_config,
|
||||
engine._reflect_llm_config,
|
||||
engine._consolidation_llm_config,
|
||||
):
|
||||
assert cfg.groq_service_tier == "flex"
|
||||
assert cfg.openai_service_tier == "flex"
|
||||
|
||||
clear_config_cache()
|
||||
|
||||
def test_memory_engine_with_explicit_params(self):
|
||||
"""Test that explicit params override env config."""
|
||||
from hindsight_api import MemoryEngine
|
||||
@@ -103,7 +128,6 @@ class TestPerOperationLLMConfig:
|
||||
reflect_llm_provider="mock",
|
||||
reflect_llm_model="explicit-reflect",
|
||||
skip_llm_verification=True,
|
||||
lazy_reranker=True,
|
||||
)
|
||||
|
||||
assert engine._llm_config.model == "explicit-default"
|
||||
@@ -126,7 +150,6 @@ class TestPerOperationLLMConfig:
|
||||
|
||||
engine = MemoryEngine(
|
||||
skip_llm_verification=True,
|
||||
lazy_reranker=True,
|
||||
)
|
||||
|
||||
# All should fall back to default
|
||||
@@ -240,7 +263,6 @@ class TestRetainUsesRetainLLMConfig:
|
||||
reflect_llm_provider="mock",
|
||||
reflect_llm_model="reflect-specific-model",
|
||||
skip_llm_verification=True,
|
||||
lazy_reranker=True,
|
||||
)
|
||||
|
||||
# Verify the retain LLM config is set correctly
|
||||
@@ -266,7 +288,6 @@ class TestReflectUsesReflectLLMConfig:
|
||||
reflect_llm_provider="mock",
|
||||
reflect_llm_model="reflect-specific-model",
|
||||
skip_llm_verification=True,
|
||||
lazy_reranker=True,
|
||||
)
|
||||
|
||||
# Verify the reflect LLM config is set correctly
|
||||
@@ -292,7 +313,6 @@ class TestReflectUsesReflectLLMConfig:
|
||||
reflect_llm_provider="mock",
|
||||
reflect_llm_model="reflect-specific-model",
|
||||
skip_llm_verification=True,
|
||||
lazy_reranker=True,
|
||||
)
|
||||
|
||||
engine._authenticate_tenant = AsyncMock() # type: ignore[method-assign]
|
||||
|
||||
@@ -69,6 +69,25 @@ class TestCleanAnswerText:
|
||||
cleaned = _clean_answer_text(text)
|
||||
assert cleaned == "Summary of findings."
|
||||
|
||||
def test_clean_text_recovers_leaked_done_arguments(self):
|
||||
"""A done tool-call argument object rendered as text should keep only answer."""
|
||||
text = """{
|
||||
"answer": "Use the inbound table for API consumers.",
|
||||
"directive_compliance": "Directive 1 followed.",
|
||||
"memory_ids": ["mem-1"],
|
||||
"mental_model_ids": [],
|
||||
"observation_ids": []
|
||||
}"""
|
||||
cleaned = _clean_answer_text(text)
|
||||
assert cleaned == "Use the inbound table for API consumers."
|
||||
assert "directive_compliance" not in cleaned
|
||||
|
||||
def test_clean_text_leaves_non_done_json_answer_unchanged(self):
|
||||
"""Plain JSON answers are valid user-visible content."""
|
||||
text = '{"status": "ok", "items": [1, 2]}'
|
||||
cleaned = _clean_answer_text(text)
|
||||
assert cleaned == text
|
||||
|
||||
|
||||
class TestCleanDoneAnswer:
|
||||
"""Test cleanup of answer field from done() tool call that leaks structured output."""
|
||||
@@ -142,6 +161,37 @@ class TestCleanDoneAnswer:
|
||||
assert "Point 2" in cleaned
|
||||
assert "mental_model_ids" not in cleaned
|
||||
|
||||
def test_clean_answer_recovers_leaked_done_arguments(self):
|
||||
"""A done answer that contains leaked done arguments should keep answer content."""
|
||||
text = """{
|
||||
"answer": "Render two markdown tables: inbound and outbound.",
|
||||
"directive_compliance": "All directives followed.",
|
||||
"memory_ids": ["mem-1", "mem-2"],
|
||||
"mental_model_ids": [],
|
||||
"observation_ids": ["obs-1"]
|
||||
}"""
|
||||
cleaned = _clean_done_answer(text)
|
||||
assert cleaned == "Render two markdown tables: inbound and outbound."
|
||||
|
||||
def test_clean_answer_recovers_fenced_leaked_done_arguments(self):
|
||||
"""Some providers put leaked done arguments in a JSON code fence."""
|
||||
text = """```json
|
||||
{
|
||||
"answer": "The current interface is HTTP only.",
|
||||
"memory_ids": [],
|
||||
"mental_model_ids": [],
|
||||
"observation_ids": []
|
||||
}
|
||||
```"""
|
||||
cleaned = _clean_done_answer(text)
|
||||
assert cleaned == "The current interface is HTTP only."
|
||||
|
||||
def test_clean_answer_rejects_done_arguments_with_unexpected_keys(self):
|
||||
"""Avoid rewriting user-requested JSON that happens to contain answer."""
|
||||
text = '{"answer": "yes", "payload": {"format": "json"}, "memory_ids": []}'
|
||||
cleaned = _clean_done_answer(text)
|
||||
assert cleaned == text
|
||||
|
||||
|
||||
class TestToolNameNormalization:
|
||||
"""Test tool name normalization for various LLM output formats."""
|
||||
|
||||
@@ -13,6 +13,7 @@ from hindsight_api._vector_index import (
|
||||
validate_extension,
|
||||
)
|
||||
from hindsight_api.engine.retain import bank_utils
|
||||
from hindsight_api.migrations import _bootstrap_vector_extension_for_migrations
|
||||
|
||||
|
||||
class RecordingConn:
|
||||
@@ -69,6 +70,25 @@ def test_bootstrap_extension_scann_installs_vector_before_alloydb_scann():
|
||||
]
|
||||
|
||||
|
||||
def test_migration_bootstrap_vchord_skips_pgvector_preflight():
|
||||
conn = RecordingConn()
|
||||
|
||||
_bootstrap_vector_extension_for_migrations(conn, "vchord")
|
||||
|
||||
assert conn.statements == ["CREATE EXTENSION IF NOT EXISTS vchord CASCADE"]
|
||||
|
||||
|
||||
def test_migration_bootstrap_scann_uses_dispatcher_without_legacy_pgvector_check():
|
||||
conn = RecordingConn()
|
||||
|
||||
_bootstrap_vector_extension_for_migrations(conn, "scann")
|
||||
|
||||
assert conn.statements == [
|
||||
"CREATE EXTENSION IF NOT EXISTS vector",
|
||||
"CREATE EXTENSION IF NOT EXISTS alloydb_scann CASCADE",
|
||||
]
|
||||
|
||||
|
||||
def test_scann_index_creation_defers_until_table_is_large_enough():
|
||||
assert should_defer_index_creation("scann", 0)
|
||||
assert should_defer_index_creation("scann", SCANN_MIN_ROWS_FOR_AUTO_INDEX - 1)
|
||||
|
||||
@@ -121,7 +121,7 @@ This runs a stdio-based MCP server that can be used directly with MCP-compatible
|
||||
- **Entity Graph** — Automatic entity extraction and relationship tracking
|
||||
- **Temporal Reasoning** — Native support for time-based queries
|
||||
- **Disposition Traits** — Configurable skepticism, literalism, and empathy influence opinion formation
|
||||
- **Three Memory Types** — World facts, bank actions, and formed opinions with confidence scores
|
||||
- **Three Memory Types** — World facts, experience facts (the bank's own actions), and observations
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hindsight-api"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
description = "Hindsight: Agent Memory That Works Like Human Memory"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"hindsight-api-slim[all]==0.8.3",
|
||||
"hindsight-api-slim[all]==0.8.4",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hindsight-cli"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
edition = "2021"
|
||||
authors = ["Hindsight Team"]
|
||||
description = "A beautiful CLI for Hindsight - semantic memory system"
|
||||
|
||||
@@ -153,7 +153,9 @@ impl ApiClient {
|
||||
|
||||
pub fn get_stats(&self, agent_id: &str, _verbose: bool) -> Result<AgentStats> {
|
||||
self.runtime.block_on(async {
|
||||
let response = self.client.get_agent_stats(agent_id, None).await?;
|
||||
// Third arg is the `refresh` query param (force fresh stats); the CLI
|
||||
// always reads the cached value, so pass None.
|
||||
let response = self.client.get_agent_stats(agent_id, None, None).await?;
|
||||
let value = response.into_inner();
|
||||
// Convert to JSON Value first, then parse into our type
|
||||
let json_value = serde_json::to_value(&value)?;
|
||||
|
||||
@@ -180,12 +180,9 @@ impl App {
|
||||
query_receiver: None,
|
||||
};
|
||||
|
||||
// Select first item by default
|
||||
// Select first bank by default. Headered lists select their first data
|
||||
// row after data is loaded.
|
||||
app.banks_state.select(Some(0));
|
||||
app.memories_state.select(Some(0));
|
||||
app.entities_state.select(Some(0));
|
||||
app.documents_state.select(Some(0));
|
||||
app.query_results_state.select(Some(0));
|
||||
|
||||
app
|
||||
}
|
||||
@@ -255,9 +252,7 @@ impl App {
|
||||
)?;
|
||||
self.memories = response.items;
|
||||
|
||||
if !self.memories.is_empty() && self.memories_state.selected().is_none() {
|
||||
self.memories_state.select(Some(0));
|
||||
}
|
||||
normalize_headered_selection(&mut self.memories_state, self.memories.len());
|
||||
|
||||
self.status_message = format!("Loaded {} memories (limit: {}, offset: {})",
|
||||
self.memories.len(), self.memories_limit, self.memories_offset);
|
||||
@@ -286,9 +281,7 @@ impl App {
|
||||
let response = self.client.list_entities(bank_id, Some(100), None, false)?;
|
||||
self.entities = response.items;
|
||||
|
||||
if !self.entities.is_empty() && self.entities_state.selected().is_none() {
|
||||
self.entities_state.select(Some(0));
|
||||
}
|
||||
normalize_headered_selection(&mut self.entities_state, self.entities.len());
|
||||
|
||||
self.status_message = format!("Loaded {} entities", self.entities.len());
|
||||
Ok(())
|
||||
@@ -298,9 +291,7 @@ impl App {
|
||||
let response = self.client.list_documents(bank_id, None, Some(100), Some(0), false)?;
|
||||
self.documents = response.items;
|
||||
|
||||
if !self.documents.is_empty() && self.documents_state.selected().is_none() {
|
||||
self.documents_state.select(Some(0));
|
||||
}
|
||||
normalize_headered_selection(&mut self.documents_state, self.documents.len());
|
||||
|
||||
self.status_message = format!("Loaded {} documents", self.documents.len());
|
||||
Ok(())
|
||||
@@ -386,9 +377,7 @@ impl App {
|
||||
match receiver.try_recv() {
|
||||
Ok(QueryResult::Recall(Ok(results))) => {
|
||||
self.query_results = results;
|
||||
if !self.query_results.is_empty() {
|
||||
self.query_results_state.select(Some(0));
|
||||
}
|
||||
normalize_headered_selection(&mut self.query_results_state, self.query_results.len());
|
||||
self.loading = false;
|
||||
self.status_message = format!("Found {} results", self.query_results.len());
|
||||
self.query_receiver = None;
|
||||
@@ -478,57 +467,17 @@ impl App {
|
||||
self.banks_state.select(Some(i));
|
||||
}
|
||||
View::Memories(_) => {
|
||||
let i = match self.memories_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.memories.len().saturating_sub(1) {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.memories_state.select(Some(i));
|
||||
select_next_headered_row(&mut self.memories_state, self.memories.len());
|
||||
}
|
||||
View::Entities(_) => {
|
||||
let i = match self.entities_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.entities.len().saturating_sub(1) {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.entities_state.select(Some(i));
|
||||
select_next_headered_row(&mut self.entities_state, self.entities.len());
|
||||
}
|
||||
View::Documents(_) => {
|
||||
let i = match self.documents_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.documents.len().saturating_sub(1) {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.documents_state.select(Some(i));
|
||||
select_next_headered_row(&mut self.documents_state, self.documents.len());
|
||||
}
|
||||
View::Query(_) => {
|
||||
if self.query_mode == QueryMode::Recall {
|
||||
let i = match self.query_results_state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.query_results.len().saturating_sub(1) {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.query_results_state.select(Some(i));
|
||||
select_next_headered_row(&mut self.query_results_state, self.query_results.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -550,57 +499,17 @@ impl App {
|
||||
self.banks_state.select(Some(i));
|
||||
}
|
||||
View::Memories(_) => {
|
||||
let i = match self.memories_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.memories.len().saturating_sub(1)
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.memories_state.select(Some(i));
|
||||
select_previous_headered_row(&mut self.memories_state, self.memories.len());
|
||||
}
|
||||
View::Entities(_) => {
|
||||
let i = match self.entities_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.entities.len().saturating_sub(1)
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.entities_state.select(Some(i));
|
||||
select_previous_headered_row(&mut self.entities_state, self.entities.len());
|
||||
}
|
||||
View::Documents(_) => {
|
||||
let i = match self.documents_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.documents.len().saturating_sub(1)
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.documents_state.select(Some(i));
|
||||
select_previous_headered_row(&mut self.documents_state, self.documents.len());
|
||||
}
|
||||
View::Query(_) => {
|
||||
if self.query_mode == QueryMode::Recall {
|
||||
let i = match self.query_results_state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.query_results.len().saturating_sub(1)
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.query_results_state.select(Some(i));
|
||||
select_previous_headered_row(&mut self.query_results_state, self.query_results.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -620,7 +529,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
View::Memories(_) => {
|
||||
if let Some(i) = self.memories_state.selected() {
|
||||
if let Some(i) = selected_headered_data_index(&self.memories_state) {
|
||||
if let Some(memory) = self.memories.get(i) {
|
||||
self.viewing_memory = Some(memory.clone());
|
||||
self.status_message = "Viewing memory details (Esc to close)".to_string();
|
||||
@@ -628,7 +537,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
View::Entities(_) => {
|
||||
if let Some(i) = self.entities_state.selected() {
|
||||
if let Some(i) = selected_headered_data_index(&self.entities_state) {
|
||||
if let Some(entity) = self.entities.get(i).cloned() {
|
||||
self.viewing_entity = Some(entity);
|
||||
self.status_message = "Viewing entity details (Esc to close)".to_string();
|
||||
@@ -636,7 +545,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
View::Documents(bank_id) => {
|
||||
if let Some(i) = self.documents_state.selected() {
|
||||
if let Some(i) = selected_headered_data_index(&self.documents_state) {
|
||||
if let Some(doc) = self.documents.get(i) {
|
||||
// Fetch full document content
|
||||
let doc_id = doc.get("id")
|
||||
@@ -664,7 +573,7 @@ impl App {
|
||||
View::Query(_) => {
|
||||
// View recall result details if in recall mode
|
||||
if self.query_mode == QueryMode::Recall {
|
||||
if let Some(i) = self.query_results_state.selected() {
|
||||
if let Some(i) = selected_headered_data_index(&self.query_results_state) {
|
||||
if let Some(result) = self.query_results.get(i).cloned() {
|
||||
self.viewing_recall_result = Some(result);
|
||||
self.status_message = "Viewing recall result (Esc to close)".to_string();
|
||||
@@ -717,7 +626,7 @@ impl App {
|
||||
|
||||
fn delete_selected_document(&mut self) -> Result<()> {
|
||||
if let View::Documents(bank_id) = &self.view {
|
||||
if let Some(i) = self.documents_state.selected() {
|
||||
if let Some(i) = selected_headered_data_index(&self.documents_state) {
|
||||
if let Some(doc) = self.documents.get(i) {
|
||||
let doc_id = doc.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -741,6 +650,46 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_headered_selection(state: &mut ListState, data_len: usize) {
|
||||
if data_len == 0 {
|
||||
state.select(None);
|
||||
return;
|
||||
}
|
||||
|
||||
let selected = match state.selected() {
|
||||
Some(i) if (1..=data_len).contains(&i) => i,
|
||||
Some(i) if i > data_len => data_len,
|
||||
_ => 1,
|
||||
};
|
||||
state.select(Some(selected));
|
||||
}
|
||||
|
||||
fn selected_headered_data_index(state: &ListState) -> Option<usize> {
|
||||
state.selected()?.checked_sub(1)
|
||||
}
|
||||
|
||||
fn select_next_headered_row(state: &mut ListState, data_len: usize) {
|
||||
if data_len == 0 {
|
||||
state.select(None);
|
||||
return;
|
||||
}
|
||||
|
||||
let current = selected_headered_data_index(state).unwrap_or(0);
|
||||
let next = if current + 1 >= data_len { 0 } else { current + 1 };
|
||||
state.select(Some(next + 1));
|
||||
}
|
||||
|
||||
fn select_previous_headered_row(state: &mut ListState, data_len: usize) {
|
||||
if data_len == 0 {
|
||||
state.select(None);
|
||||
return;
|
||||
}
|
||||
|
||||
let current = selected_headered_data_index(state).unwrap_or(0);
|
||||
let previous = if current == 0 { data_len - 1 } else { current - 1 };
|
||||
state.select(Some(previous + 1));
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -982,7 +931,8 @@ fn render_memories(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(7), // Memory metadata
|
||||
Constraint::Min(0), // Full text content
|
||||
Constraint::Percentage(45), // Full text content
|
||||
Constraint::Percentage(55), // Complete JSON details
|
||||
])
|
||||
.split(area);
|
||||
|
||||
@@ -1020,6 +970,13 @@ fn render_memories(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.style(Style::default().fg(Color::White));
|
||||
|
||||
f.render_widget(content_widget, chunks[1]);
|
||||
|
||||
let details_widget = Paragraph::new(format_memory_details_json(memory))
|
||||
.block(Block::default().borders(Borders::ALL).title("Details JSON"))
|
||||
.wrap(Wrap { trim: false })
|
||||
.style(Style::default().fg(Color::White));
|
||||
|
||||
f.render_widget(details_widget, chunks[2]);
|
||||
} else {
|
||||
// Show memory list as table
|
||||
let mut items = vec![
|
||||
@@ -1063,6 +1020,11 @@ fn render_memories(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_memory_details_json(memory: &Map<String, Value>) -> String {
|
||||
serde_json::to_string_pretty(memory)
|
||||
.unwrap_or_else(|_| "Unable to render memory details".to_string())
|
||||
}
|
||||
|
||||
fn render_entities(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
// If viewing an entity, show its details
|
||||
if let Some(entity) = &app.viewing_entity {
|
||||
@@ -1568,3 +1530,75 @@ pub fn run(client: &ApiClient) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn headered_selection_maps_visible_rows_to_data_indices() {
|
||||
let mut state = ListState::default();
|
||||
|
||||
normalize_headered_selection(&mut state, 3);
|
||||
assert_eq!(state.selected(), Some(1));
|
||||
assert_eq!(selected_headered_data_index(&state), Some(0));
|
||||
|
||||
select_next_headered_row(&mut state, 3);
|
||||
assert_eq!(state.selected(), Some(2));
|
||||
assert_eq!(selected_headered_data_index(&state), Some(1));
|
||||
|
||||
select_next_headered_row(&mut state, 3);
|
||||
assert_eq!(state.selected(), Some(3));
|
||||
assert_eq!(selected_headered_data_index(&state), Some(2));
|
||||
|
||||
select_next_headered_row(&mut state, 3);
|
||||
assert_eq!(state.selected(), Some(1));
|
||||
assert_eq!(selected_headered_data_index(&state), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headered_selection_never_selects_the_header_row() {
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(0));
|
||||
|
||||
normalize_headered_selection(&mut state, 2);
|
||||
assert_eq!(state.selected(), Some(1));
|
||||
|
||||
select_previous_headered_row(&mut state, 2);
|
||||
assert_eq!(state.selected(), Some(2));
|
||||
assert_eq!(selected_headered_data_index(&state), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headered_selection_clears_when_no_data_exists() {
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(2));
|
||||
|
||||
normalize_headered_selection(&mut state, 0);
|
||||
assert_eq!(state.selected(), None);
|
||||
|
||||
select_next_headered_row(&mut state, 0);
|
||||
assert_eq!(state.selected(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_details_json_includes_non_text_attributes() {
|
||||
let memory = serde_json::json!({
|
||||
"id": "mem_123",
|
||||
"fact_type": "world",
|
||||
"text": "Alice works at Google",
|
||||
"entities": [
|
||||
{"canonical_name": "Alice", "type": "person"}
|
||||
],
|
||||
"metadata": {
|
||||
"source": "import"
|
||||
}
|
||||
});
|
||||
|
||||
let details = format_memory_details_json(memory.as_object().unwrap());
|
||||
|
||||
assert!(details.contains("\"id\": \"mem_123\""));
|
||||
assert!(details.contains("\"entities\""));
|
||||
assert!(details.contains("\"metadata\""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.8.3
|
||||
version: 0.8.4
|
||||
servers:
|
||||
- url: /
|
||||
paths:
|
||||
@@ -583,6 +583,19 @@ paths:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- description: "Force a fresh recompute, bypassing the cached value (and refreshing\
|
||||
\ the cache)."
|
||||
explode: true
|
||||
in: query
|
||||
name: refresh
|
||||
required: false
|
||||
schema:
|
||||
default: false
|
||||
description: "Force a fresh recompute, bypassing the cached value (and refreshing\
|
||||
\ the cache)."
|
||||
title: Refresh
|
||||
type: boolean
|
||||
style: form
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
@@ -1369,6 +1382,348 @@ paths:
|
||||
summary: Clear mental model content
|
||||
tags:
|
||||
- Mental Models
|
||||
/v1/default/banks/{bank_id}/knowledge-base/tree:
|
||||
get:
|
||||
description: Return the knowledge base as a nested tree of folders and pages.
|
||||
operationId: get_knowledge_base_tree
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank 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:
|
||||
$ref: '#/components/schemas/KnowledgeTreeResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Get the knowledge-base tree
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/folders:
|
||||
post:
|
||||
description: "Create a folder, optionally nested under a parent folder."
|
||||
operationId: create_knowledge_folder
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateFolderRequest'
|
||||
required: true
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KnowledgeNode'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Create a knowledge-base folder
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/pages:
|
||||
post:
|
||||
description: Create a page (a mental model + tree node). Content is generated
|
||||
asynchronously; use the returned operation_id to track completion.
|
||||
operationId: create_knowledge_page
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreatePageRequest'
|
||||
required: true
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateKnowledgePageResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Create a knowledge-base page
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/graph:
|
||||
get:
|
||||
description: "Return pages as nodes linked by shared tags, for the constellation\
|
||||
\ view."
|
||||
operationId: get_knowledge_base_graph
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank 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:
|
||||
$ref: '#/components/schemas/KnowledgePageGraphResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Knowledge-base constellation graph
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/export:
|
||||
get:
|
||||
description: "Return a portable OKF bundle: a nested index.md, one <id>.md per\
|
||||
\ page, and history logs."
|
||||
operationId: export_knowledge_base
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank 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:
|
||||
$ref: '#/components/schemas/KnowledgePageBundleResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Export the knowledge base as an OKF bundle
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}:
|
||||
get:
|
||||
description: Return a single page as an OKF document (frontmatter + markdown
|
||||
body).
|
||||
operationId: get_knowledge_page
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: path
|
||||
name: page_id
|
||||
required: true
|
||||
schema:
|
||||
title: Page 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:
|
||||
$ref: '#/components/schemas/KnowledgePageResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Get a knowledge-base page
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}:
|
||||
delete:
|
||||
description: Delete a folder or page and its whole subtree (pages' mental models
|
||||
are removed too).
|
||||
operationId: delete_knowledge_node
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: path
|
||||
name: node_id
|
||||
required: true
|
||||
schema:
|
||||
title: Node 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: Delete a knowledge-base node
|
||||
tags:
|
||||
- Knowledge Base
|
||||
patch:
|
||||
description: "Rename a node (set `name`) and/or move it under another folder\
|
||||
\ (set `parent_id`, null for the root)."
|
||||
operationId: update_knowledge_node
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: path
|
||||
name: node_id
|
||||
required: true
|
||||
schema:
|
||||
title: Node Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateNodeRequest'
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KnowledgeNode'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Rename or move a knowledge-base node
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/directives:
|
||||
get:
|
||||
description: List hard rules that are injected into prompts.
|
||||
@@ -5082,6 +5437,42 @@ components:
|
||||
- content
|
||||
- name
|
||||
title: CreateDirectiveRequest
|
||||
CreateFolderRequest:
|
||||
description: Create a folder under an optional parent folder.
|
||||
example:
|
||||
parent_id: parent_id
|
||||
name: name
|
||||
properties:
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
parent_id:
|
||||
nullable: true
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
title: CreateFolderRequest
|
||||
CreateKnowledgePageResponse:
|
||||
description: "Result of creating a page: the node id, its mental model, and\
|
||||
\ the refresh op."
|
||||
example:
|
||||
page_id: page_id
|
||||
operation_id: operation_id
|
||||
mental_model_id: mental_model_id
|
||||
properties:
|
||||
page_id:
|
||||
title: Page Id
|
||||
type: string
|
||||
mental_model_id:
|
||||
title: Mental Model Id
|
||||
type: string
|
||||
operation_id:
|
||||
nullable: true
|
||||
type: string
|
||||
required:
|
||||
- mental_model_id
|
||||
- page_id
|
||||
title: CreateKnowledgePageResponse
|
||||
CreateMentalModelRequest:
|
||||
description: Request model for creating a mental model.
|
||||
example:
|
||||
@@ -5140,6 +5531,64 @@ components:
|
||||
required:
|
||||
- operation_id
|
||||
title: CreateMentalModelResponse
|
||||
CreatePageRequest:
|
||||
description: Create a page (a mental model + tree node) under an optional parent
|
||||
folder.
|
||||
example:
|
||||
source_query: source_query
|
||||
max_tokens: 0
|
||||
parent_id: parent_id
|
||||
name: name
|
||||
trigger:
|
||||
mode: full
|
||||
refresh_after_consolidation: false
|
||||
recall_chunks_max_tokens: 1
|
||||
tag_groups:
|
||||
- match: any_strict
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
- match: any_strict
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
fact_types:
|
||||
- world
|
||||
- world
|
||||
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
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
properties:
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
source_query:
|
||||
title: Source Query
|
||||
type: string
|
||||
parent_id:
|
||||
nullable: true
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
max_tokens:
|
||||
nullable: true
|
||||
type: integer
|
||||
trigger:
|
||||
$ref: '#/components/schemas/MentalModelTrigger-Input'
|
||||
required:
|
||||
- name
|
||||
- source_query
|
||||
title: CreatePageRequest
|
||||
CreateWebhookRequest:
|
||||
description: Request model for registering a webhook.
|
||||
example:
|
||||
@@ -5943,6 +6392,226 @@ components:
|
||||
source_facts:
|
||||
$ref: '#/components/schemas/SourceFactsIncludeOptions'
|
||||
title: IncludeOptions
|
||||
KnowledgeNode:
|
||||
description: |-
|
||||
A node in the knowledge-base tree — a folder or a page.
|
||||
|
||||
Pages carry ``description``/``tags`` from their backing mental model. The
|
||||
knowledge base is client-managed (CRUD); ``managed`` lets a client tag a node
|
||||
as system-owned vs. hand-authored.
|
||||
example:
|
||||
children:
|
||||
- null
|
||||
- null
|
||||
kind: folder
|
||||
parent_id: parent_id
|
||||
managed: false
|
||||
name: name
|
||||
description: description
|
||||
id: id
|
||||
mental_model_id: mental_model_id
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
timestamp: timestamp
|
||||
properties:
|
||||
id:
|
||||
title: Id
|
||||
type: string
|
||||
kind:
|
||||
enum:
|
||||
- folder
|
||||
- page
|
||||
title: Kind
|
||||
type: string
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
parent_id:
|
||||
nullable: true
|
||||
type: string
|
||||
mental_model_id:
|
||||
nullable: true
|
||||
type: string
|
||||
managed:
|
||||
default: false
|
||||
description: "Client-set flag: true = system-owned, false = hand-authored."
|
||||
title: Managed
|
||||
type: boolean
|
||||
description:
|
||||
nullable: true
|
||||
type: string
|
||||
tags:
|
||||
default: []
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
timestamp:
|
||||
nullable: true
|
||||
type: string
|
||||
children:
|
||||
default: []
|
||||
items:
|
||||
$ref: '#/components/schemas/KnowledgeNode'
|
||||
type: array
|
||||
required:
|
||||
- id
|
||||
- kind
|
||||
- name
|
||||
title: KnowledgeNode
|
||||
KnowledgePageBundleFile:
|
||||
description: One file in a portable OKF bundle.
|
||||
example:
|
||||
path: path
|
||||
content: content
|
||||
properties:
|
||||
path:
|
||||
title: Path
|
||||
type: string
|
||||
content:
|
||||
title: Content
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- path
|
||||
title: KnowledgePageBundleFile
|
||||
KnowledgePageBundleResponse:
|
||||
description: A portable OKF bundle — a flat set of markdown files (index + pages
|
||||
+ logs).
|
||||
example:
|
||||
files:
|
||||
- path: path
|
||||
content: content
|
||||
- path: path
|
||||
content: content
|
||||
properties:
|
||||
files:
|
||||
items:
|
||||
$ref: '#/components/schemas/KnowledgePageBundleFile'
|
||||
type: array
|
||||
required:
|
||||
- files
|
||||
title: KnowledgePageBundleResponse
|
||||
KnowledgePageGraphResponse:
|
||||
description: Constellation graph of knowledge pages linked by shared tags.
|
||||
example:
|
||||
total_edges: 6
|
||||
nodes:
|
||||
- key: ""
|
||||
- key: ""
|
||||
edges:
|
||||
- key: ""
|
||||
- key: ""
|
||||
total_pages: 0
|
||||
properties:
|
||||
nodes:
|
||||
items:
|
||||
additionalProperties: {}
|
||||
type: array
|
||||
edges:
|
||||
items:
|
||||
additionalProperties: {}
|
||||
type: array
|
||||
total_pages:
|
||||
title: Total Pages
|
||||
type: integer
|
||||
total_edges:
|
||||
title: Total Edges
|
||||
type: integer
|
||||
required:
|
||||
- edges
|
||||
- nodes
|
||||
- total_edges
|
||||
- total_pages
|
||||
title: KnowledgePageGraphResponse
|
||||
KnowledgePageResponse:
|
||||
description: A knowledge page rendered as an OKF document.
|
||||
example:
|
||||
name: name
|
||||
markdown: markdown
|
||||
description: description
|
||||
id: id
|
||||
type: type
|
||||
body: body
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
timestamp: timestamp
|
||||
properties:
|
||||
id:
|
||||
title: Id
|
||||
type: string
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
type:
|
||||
description: "OKF document type — from a `type:<x>` tag, else 'knowledge-page'."
|
||||
title: Type
|
||||
type: string
|
||||
description:
|
||||
nullable: true
|
||||
type: string
|
||||
tags:
|
||||
default: []
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
timestamp:
|
||||
nullable: true
|
||||
type: string
|
||||
body:
|
||||
nullable: true
|
||||
type: string
|
||||
markdown:
|
||||
description: "The full OKF document: YAML frontmatter + markdown body."
|
||||
title: Markdown
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- markdown
|
||||
- name
|
||||
- type
|
||||
title: KnowledgePageResponse
|
||||
KnowledgeTreeResponse:
|
||||
description: The knowledge base as a nested folder/page tree.
|
||||
example:
|
||||
roots:
|
||||
- children:
|
||||
- null
|
||||
- null
|
||||
kind: folder
|
||||
parent_id: parent_id
|
||||
managed: false
|
||||
name: name
|
||||
description: description
|
||||
id: id
|
||||
mental_model_id: mental_model_id
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
timestamp: timestamp
|
||||
- children:
|
||||
- null
|
||||
- null
|
||||
kind: folder
|
||||
parent_id: parent_id
|
||||
managed: false
|
||||
name: name
|
||||
description: description
|
||||
id: id
|
||||
mental_model_id: mental_model_id
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
timestamp: timestamp
|
||||
properties:
|
||||
roots:
|
||||
items:
|
||||
$ref: '#/components/schemas/KnowledgeNode'
|
||||
type: array
|
||||
required:
|
||||
- roots
|
||||
title: KnowledgeTreeResponse
|
||||
LLMRequestEntry:
|
||||
description: "A single LLM request trace row, as returned by the read API."
|
||||
example:
|
||||
@@ -6728,6 +7397,29 @@ components:
|
||||
title: MentalModelResponse
|
||||
MentalModelTrigger-Input:
|
||||
description: Trigger settings for a mental model.
|
||||
example:
|
||||
mode: full
|
||||
refresh_after_consolidation: false
|
||||
recall_chunks_max_tokens: 1
|
||||
tag_groups:
|
||||
- match: any_strict
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
- match: any_strict
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
fact_types:
|
||||
- world
|
||||
- world
|
||||
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:
|
||||
mode:
|
||||
default: full
|
||||
@@ -8101,6 +8793,19 @@ components:
|
||||
trigger:
|
||||
$ref: '#/components/schemas/MentalModelTrigger-Input'
|
||||
title: UpdateMentalModelRequest
|
||||
UpdateNodeRequest:
|
||||
description: Rename and/or move a node. Each field applies only when present.
|
||||
example:
|
||||
parent_id: parent_id
|
||||
name: name
|
||||
properties:
|
||||
name:
|
||||
nullable: true
|
||||
type: string
|
||||
parent_id:
|
||||
nullable: true
|
||||
type: string
|
||||
title: UpdateNodeRequest
|
||||
UpdateWebhookRequest:
|
||||
description: Request model for updating a webhook. Only provided fields are
|
||||
updated.
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
API version: 0.8.4
|
||||
*/
|
||||
|
||||
// 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.8.3
|
||||
API version: 0.8.4
|
||||
*/
|
||||
|
||||
// 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.8.3
|
||||
API version: 0.8.4
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
@@ -540,9 +540,16 @@ type ApiGetAgentStatsRequest struct {
|
||||
ctx context.Context
|
||||
ApiService *BanksAPIService
|
||||
bankId string
|
||||
refresh *bool
|
||||
authorization *string
|
||||
}
|
||||
|
||||
// Force a fresh recompute, bypassing the cached value (and refreshing the cache).
|
||||
func (r ApiGetAgentStatsRequest) Refresh(refresh bool) ApiGetAgentStatsRequest {
|
||||
r.refresh = &refresh
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ApiGetAgentStatsRequest) Authorization(authorization string) ApiGetAgentStatsRequest {
|
||||
r.authorization = &authorization
|
||||
return r
|
||||
@@ -591,6 +598,12 @@ func (a *BanksAPIService) GetAgentStatsExecute(r ApiGetAgentStatsRequest) (*Bank
|
||||
localVarQueryParams := url.Values{}
|
||||
localVarFormParams := url.Values{}
|
||||
|
||||
if r.refresh != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "refresh", r.refresh, "form", "")
|
||||
} else {
|
||||
var defaultValue bool = false
|
||||
r.refresh = &defaultValue
|
||||
}
|
||||
// to determine the Content-Type header
|
||||
localVarHTTPContentTypes := []string{}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
API version: 0.8.4
|
||||
*/
|
||||
|
||||
// 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.8.3
|
||||
API version: 0.8.4
|
||||
*/
|
||||
|
||||
// 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.8.3
|
||||
API version: 0.8.4
|
||||
*/
|
||||
|
||||
// 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.8.3
|
||||
API version: 0.8.4
|
||||
*/
|
||||
|
||||
// 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