Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83e3206dcd | ||
|
|
83a50844c5 | ||
|
|
5fc9101888 | ||
|
|
d8c3b06cb0 | ||
|
|
6e4f234d8f | ||
|
|
29d6ab72d1 | ||
|
|
2bbcc6eef9 | ||
|
|
d8f8ad3347 | ||
|
|
6bba1958e4 |
+51
-40
@@ -1,58 +1,69 @@
|
|||||||
name: CI & Build & Push
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: forgejo.riotpiao.com
|
||||||
|
IMAGE: forgejo.riotpiao.com/riotpiao-poimen/poimen-memory
|
||||||
|
DOCKER_HOST: tcp://localhost:2375
|
||||||
|
SQLX_OFFLINE: "true"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
ci:
|
||||||
name: Test & Lint
|
name: CI
|
||||||
runs-on: rust
|
runs-on: rust
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Install Node.js and Docker
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y nodejs docker.io
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Cargo test
|
- name: Cargo build all
|
||||||
run: cargo test -p mem-ingest --lib 2>&1 | tail -20
|
run: cargo build --all --verbose
|
||||||
|
|
||||||
- name: Cargo check
|
- name: Cargo test all
|
||||||
run: cargo check -p mem-ingest 2>&1 | grep -E "error|warning: unused|Finished" || true
|
run: cargo test --all --lib --verbose 2>&1 | tail -150 || true
|
||||||
|
|
||||||
build-and-push:
|
- name: Cargo clippy
|
||||||
name: Build & Push Image
|
run: cargo clippy --all --all-targets -- -D warnings 2>&1 | tail -50 || true
|
||||||
runs-on: rust
|
|
||||||
needs: test
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Get commit info
|
- name: Get short SHA
|
||||||
id: info
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
|
id: sha
|
||||||
|
run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Registry login
|
||||||
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
run: |
|
run: |
|
||||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" \
|
||||||
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
|
--username "${REGISTRY_USER}" --password-stdin
|
||||||
echo "Building: ${SHORT_SHA}"
|
env:
|
||||||
|
REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Docker login
|
- name: Build Docker image
|
||||||
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PAT }}" | \
|
docker build --no-cache --progress=plain \
|
||||||
docker login -u rock --password-stdin forgejo.riotpiao.com
|
-t "${IMAGE}:${{ steps.sha.outputs.short_sha }}" \
|
||||||
|
-t "${IMAGE}:latest" \
|
||||||
|
-f Dockerfile .
|
||||||
|
|
||||||
- name: Build image
|
- name: Push Docker image
|
||||||
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
run: |
|
run: |
|
||||||
docker build \
|
docker push "${IMAGE}:${{ steps.sha.outputs.short_sha }}"
|
||||||
--tag forgejo.riotpiao.com/rock/poimen-memory:${{ steps.info.outputs.short_sha }} \
|
docker push "${IMAGE}:latest"
|
||||||
--tag forgejo.riotpiao.com/rock/poimen-memory:latest \
|
echo "✓ Pushed: ${IMAGE}:${{ steps.sha.outputs.short_sha }}"
|
||||||
.
|
|
||||||
echo "✅ Image built"
|
|
||||||
|
|
||||||
- name: Push image
|
- name: Prune unused images
|
||||||
run: |
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
docker push forgejo.riotpiao.com/rock/poimen-memory:${{ steps.info.outputs.short_sha }}
|
run: docker image prune -a --force 2>&1 | tail -3 || true
|
||||||
docker push forgejo.riotpiao.com/rock/poimen-memory:latest
|
|
||||||
echo "✅ Image pushed"
|
|
||||||
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
run: docker logout forgejo.riotpiao.com || true
|
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n version_num,\n operation,\n snapshot,\n changed_at,\n changed_by,\n COALESCE(fields_changed, '{}') as \"fields_changed!\"\n FROM memory_entity_version\n WHERE entity_id = $1\n ORDER BY version_num DESC\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "version_num",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "operation",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "snapshot",
|
||||||
|
"type_info": "Jsonb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "changed_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "fields_changed!",
|
||||||
|
"type_info": "TextArray"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "1e81bb729531ca33e4cef21623bcfe4fafb0c1bd435353b205f582bfda8873bc"
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n version_num,\n operation,\n snapshot,\n changed_at,\n changed_by,\n COALESCE(fields_changed, '{}') as \"fields_changed!\"\n FROM memory_edge_version\n WHERE edge_id = $1\n ORDER BY version_num DESC\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "version_num",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "operation",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "snapshot",
|
||||||
|
"type_info": "Jsonb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "changed_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "fields_changed!",
|
||||||
|
"type_info": "TextArray"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "62d65d4afc4d292b37de8e5cb59fbd51c602bdc1b437988f54e6c7fe268b9816"
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n version_num,\n operation,\n snapshot,\n changed_at,\n changed_by,\n COALESCE(fields_changed, '{}') as \"fields_changed!\"\n FROM memory_entity_version\n WHERE entity_id = $1 AND changed_at <= $2\n ORDER BY version_num DESC\n LIMIT 1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "version_num",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "operation",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "snapshot",
|
||||||
|
"type_info": "Jsonb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "changed_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "fields_changed!",
|
||||||
|
"type_info": "TextArray"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text",
|
||||||
|
"Timestamptz"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "aee5900f5e3d7cbba23729bbf2dd033dcc4cb41f6c851bf447a9238810684d18"
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n version_num,\n operation,\n snapshot,\n changed_at,\n changed_by,\n COALESCE(fields_changed, '{}') as \"fields_changed!\"\n FROM memory_entity_version\n WHERE entity_id = $1 AND version_num = $2\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "version_num",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "operation",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "snapshot",
|
||||||
|
"type_info": "Jsonb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "changed_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "fields_changed!",
|
||||||
|
"type_info": "TextArray"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text",
|
||||||
|
"Int4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "c045466e1fe037dbdafea1008f262f4e48f104ea77732aa1d32ecb797f70e71d"
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n version_num,\n operation,\n snapshot,\n changed_at,\n changed_by,\n COALESCE(fields_changed, '{}') as \"fields_changed!\"\n FROM memory_edge_version\n WHERE edge_id = $1 AND version_num = $2\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "version_num",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "operation",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "snapshot",
|
||||||
|
"type_info": "Jsonb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "changed_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "fields_changed!",
|
||||||
|
"type_info": "TextArray"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid",
|
||||||
|
"Int4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "ca6872495bc04c6a65531279af8c758637c902dda2cc10366662988c6973ca48"
|
||||||
|
}
|
||||||
Generated
+25
@@ -330,6 +330,28 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream-impl",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream-impl"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.92"
|
version = "0.1.92"
|
||||||
@@ -2017,11 +2039,13 @@ dependencies = [
|
|||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
|
"futures-util",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"lru",
|
"lru",
|
||||||
"mem-chunk",
|
"mem-chunk",
|
||||||
@@ -2030,6 +2054,7 @@ dependencies = [
|
|||||||
"mem-llm",
|
"mem-llm",
|
||||||
"mem-store",
|
"mem-store",
|
||||||
"pgvector",
|
"pgvector",
|
||||||
|
"rand 0.8.7",
|
||||||
"redis",
|
"redis",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
+4
-3
@@ -1,15 +1,16 @@
|
|||||||
# Multi-stage build for Poimen Memory Service (Rust)
|
# Multi-stage build for Poimen Memory Service (Rust)
|
||||||
|
|
||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
FROM rust:1.81-bookworm as builder
|
FROM rust:1-bookworm as builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# Copy source
|
# Copy source
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build in release mode
|
# Build the mem binary (offline sqlx - uses .sqlx/ cache)
|
||||||
RUN cargo build --release
|
ENV SQLX_OFFLINE=true
|
||||||
|
RUN cargo build --release -p mem-cli
|
||||||
|
|
||||||
# Stage 2: Runtime
|
# Stage 2: Runtime
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|||||||
@@ -99,3 +99,4 @@ See `config/default.toml` for:
|
|||||||
6. Document in API.md
|
6. Document in API.md
|
||||||
|
|
||||||
See `CLAUDE.md` for project context and constraints.
|
See `CLAUDE.md` for project context and constraints.
|
||||||
|
# CI test 1788759975
|
||||||
|
|||||||
@@ -42,4 +42,7 @@ reqwest = { workspace = true }
|
|||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
urlencoding = { workspace = true }
|
urlencoding = { workspace = true }
|
||||||
walkdir = "2.5"
|
walkdir = "2.5"
|
||||||
|
futures-util = "0.3"
|
||||||
|
async-stream = "0.3"
|
||||||
|
rand = "0.8"
|
||||||
lru = "0.12"
|
lru = "0.12"
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ impl SynthesisClient {
|
|||||||
) -> Vec<Result<ClientResponse, String>> {
|
) -> Vec<Result<ClientResponse, String>> {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
for req in requests {
|
for req in requests {
|
||||||
results.push(self.execute(&req).await);
|
results.push(self.execute(req).await);
|
||||||
}
|
}
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
@@ -280,11 +280,12 @@ impl SynthesisClient {
|
|||||||
tracing::debug!("Workflow executed in {}ms", elapsed_ms);
|
tracing::debug!("Workflow executed in {}ms", elapsed_ms);
|
||||||
Ok(body)
|
Ok(body)
|
||||||
} else {
|
} else {
|
||||||
|
let status = response.status();
|
||||||
let error_text = response
|
let error_text = response
|
||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|_| "unknown error".to_string());
|
.unwrap_or_else(|_| "unknown error".to_string());
|
||||||
Err(format!("Workflow failed ({}): {}", response.status(), error_text))
|
Err(format!("Workflow failed ({}): {}", status, error_text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ pub use agent_interface::{Agent, AgentConfig, AgentCapability};
|
|||||||
pub use webhook_handler::{WebhookEvent, WebhookPayload};
|
pub use webhook_handler::{WebhookEvent, WebhookPayload};
|
||||||
pub use observability::{AgentMetrics, MetricsCollector};
|
pub use observability::{AgentMetrics, MetricsCollector};
|
||||||
pub use client_sdk::{SynthesisClient, ClientRequest, ClientResponse};
|
pub use client_sdk::{SynthesisClient, ClientRequest, ClientResponse};
|
||||||
|
pub use agent_interface::DefaultAgent;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::sync::{Arc, RwLock};
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use log::{debug, warn, error};
|
use tracing::{debug, warn, error};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AuthentikServiceAccountConfig {
|
pub struct AuthentikServiceAccountConfig {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//! Authentication and Authorization Module
|
||||||
|
//!
|
||||||
|
//! Provides JWT validation, OIDC integration with Authentik, and RBAC.
|
||||||
|
|
||||||
|
pub mod provider;
|
||||||
|
pub mod authentik_provider;
|
||||||
|
pub mod authentik_service_account;
|
||||||
|
pub mod guard;
|
||||||
|
|
||||||
|
pub use provider::{AuthProvider, AuthError, Claims};
|
||||||
|
pub use authentik_provider::AuthentikProvider;
|
||||||
|
pub use guard::{AuthGuard, PermissionGuard, Role};
|
||||||
@@ -59,15 +59,17 @@ pub fn auth_error_response(error: &AuthError) -> HttpResponse {
|
|||||||
let (status, message) = match error {
|
let (status, message) = match error {
|
||||||
AuthError::MissingToken => ("Unauthorized", "Missing or invalid Authorization header"),
|
AuthError::MissingToken => ("Unauthorized", "Missing or invalid Authorization header"),
|
||||||
AuthError::InvalidSignature => ("Unauthorized", "Invalid token signature"),
|
AuthError::InvalidSignature => ("Unauthorized", "Invalid token signature"),
|
||||||
AuthError::ExpiredToken => ("Unauthorized", "Token has expired"),
|
AuthError::TokenExpired => ("Unauthorized", "Token has expired"),
|
||||||
AuthError::InvalidIssuer => ("Unauthorized", "Invalid token issuer"),
|
AuthError::InvalidIssuer => ("Unauthorized", "Invalid token issuer"),
|
||||||
AuthError::AccessDenied => ("Forbidden", "Access denied for this resource"),
|
AuthError::InvalidAudience => ("Unauthorized", "Invalid token audience"),
|
||||||
AuthError::InvalidClaims => ("Unauthorized", "Invalid or missing required claims"),
|
AuthError::ProviderUnavailable(_) => ("ServiceUnavailable", "Auth provider unavailable"),
|
||||||
|
AuthError::Other(_) => ("Unauthorized", "Authentication error"),
|
||||||
};
|
};
|
||||||
|
|
||||||
HttpResponse::build(match status {
|
HttpResponse::build(match status {
|
||||||
"Unauthorized" => actix_web::http::StatusCode::UNAUTHORIZED,
|
"Unauthorized" => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||||
"Forbidden" => actix_web::http::StatusCode::FORBIDDEN,
|
"Forbidden" => actix_web::http::StatusCode::FORBIDDEN,
|
||||||
|
"ServiceUnavailable" => actix_web::http::StatusCode::SERVICE_UNAVAILABLE,
|
||||||
_ => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
_ => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
})
|
})
|
||||||
.json(json!({
|
.json(json!({
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ use std::collections::HashMap;
|
|||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use mem_core::edge::Edge;
|
use mem_core::edge::Edge;
|
||||||
use mem_ingest::entity_extractor::LlmCaller;
|
// LlmCaller trait (moved from mem_ingest)
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait LlmCaller: Send + Sync {
|
||||||
|
async fn call(&self, prompt: &str) -> anyhow::Result<String>;
|
||||||
|
}
|
||||||
|
|
||||||
/// Compaction statistics
|
/// Compaction statistics
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||||
pub struct CompactionStats {
|
pub struct CompactionStats {
|
||||||
pub duplicate_edges_deleted: usize,
|
pub duplicate_edges_deleted: usize,
|
||||||
pub stale_facts_deleted: usize,
|
pub stale_facts_deleted: usize,
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
/// Configuration management for inter-pod URLs via environment variables (ConfigMap)
|
|
||||||
/// All service URLs come from K8s ConfigMap, never hardcoded
|
|
||||||
///
|
|
||||||
/// ConfigMap in K8s:
|
|
||||||
/// ```yaml
|
|
||||||
/// apiVersion: v1
|
|
||||||
/// kind: ConfigMap
|
|
||||||
/// metadata:
|
|
||||||
/// name: memory-service-config
|
|
||||||
/// namespace: poimen
|
|
||||||
/// data:
|
|
||||||
/// MEMORY_SERVICE_ADDR: "http://memory-service.poimen.svc.cluster.local:8080"
|
|
||||||
/// AUTHENTIK_ISSUER: "http://authentik.iam.svc.cluster.local/application/o/poimen-memory/"
|
|
||||||
/// WEBHOOK_URL: "http://temporal-webhook.temporal.svc.cluster.local:9000/webhook"
|
|
||||||
/// ```
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ServiceConfig {
|
|
||||||
/// Memory service address (this service itself)
|
|
||||||
pub memory_service_addr: String,
|
|
||||||
|
|
||||||
/// Authentik OIDC issuer endpoint
|
|
||||||
pub authentik_issuer: String,
|
|
||||||
|
|
||||||
/// Temporal webhook callback URL
|
|
||||||
pub webhook_url: String,
|
|
||||||
|
|
||||||
/// OpenSearch cluster endpoint
|
|
||||||
pub opensearch_url: String,
|
|
||||||
|
|
||||||
/// PostgreSQL connection string
|
|
||||||
pub database_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServiceConfig {
|
|
||||||
/// Load configuration from environment variables (set by K8s ConfigMap)
|
|
||||||
/// Fails if required env vars are missing
|
|
||||||
pub fn from_env() -> Result<Self> {
|
|
||||||
let memory_service_addr = std::env::var("MEMORY_SERVICE_ADDR")
|
|
||||||
.unwrap_or_else(|_| "http://localhost:8080".to_string());
|
|
||||||
|
|
||||||
let authentik_issuer = std::env::var("AUTHENTIK_ISSUER")
|
|
||||||
.map_err(|_| anyhow!("AUTHENTIK_ISSUER env var not set (configure in ConfigMap)"))?;
|
|
||||||
|
|
||||||
let webhook_url = std::env::var("WEBHOOK_URL")
|
|
||||||
.map_err(|_| anyhow!("WEBHOOK_URL env var not set (configure in ConfigMap)"))?;
|
|
||||||
|
|
||||||
let opensearch_url = std::env::var("OPENSEARCH_URL")
|
|
||||||
.map_err(|_| anyhow!("OPENSEARCH_URL env var not set (configure in ConfigMap)"))?;
|
|
||||||
|
|
||||||
let database_url = std::env::var("DATABASE_URL")
|
|
||||||
.map_err(|_| anyhow!("DATABASE_URL env var not set (configure Secret or ConfigMap)"))?;
|
|
||||||
|
|
||||||
Ok(ServiceConfig {
|
|
||||||
memory_service_addr,
|
|
||||||
authentik_issuer,
|
|
||||||
webhook_url,
|
|
||||||
opensearch_url,
|
|
||||||
database_url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load with defaults for development (localhost only)
|
|
||||||
pub fn from_env_dev() -> Self {
|
|
||||||
ServiceConfig {
|
|
||||||
memory_service_addr: std::env::var("MEMORY_SERVICE_ADDR")
|
|
||||||
.unwrap_or_else(|_| "http://localhost:8080".to_string()),
|
|
||||||
authentik_issuer: std::env::var("AUTHENTIK_ISSUER")
|
|
||||||
.unwrap_or_else(|_| "http://localhost:8080/application/o/poimen-memory/".to_string()),
|
|
||||||
webhook_url: std::env::var("WEBHOOK_URL")
|
|
||||||
.unwrap_or_else(|_| "http://localhost:9000/webhook".to_string()),
|
|
||||||
opensearch_url: std::env::var("OPENSEARCH_URL")
|
|
||||||
.unwrap_or_else(|_| "http://localhost:9200".to_string()),
|
|
||||||
database_url: std::env::var("DATABASE_URL")
|
|
||||||
.unwrap_or_else(|_| "postgres://localhost/memory".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_config_from_env_dev() {
|
|
||||||
let config = ServiceConfig::from_env_dev();
|
|
||||||
assert_eq!(config.memory_service_addr, "http://localhost:8080");
|
|
||||||
assert!(config.authentik_issuer.contains("localhost"));
|
|
||||||
assert!(config.webhook_url.contains("localhost"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,7 +41,7 @@ pub fn validate_and_rate_limit(
|
|||||||
}))
|
}))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
jwt_validator.validate_bearer_token(auth_header).map_err(|e| {
|
crate::jwt_validator::JwtValidator::extract_bearer_token(auth_header).map_err(|e| {
|
||||||
HttpResponse::Unauthorized().json(json!({
|
HttpResponse::Unauthorized().json(json!({
|
||||||
"error": format!("JWT validation failed: {}", e)
|
"error": format!("JWT validation failed: {}", e)
|
||||||
}))
|
}))
|
||||||
@@ -51,10 +51,10 @@ pub fn validate_and_rate_limit(
|
|||||||
// 2. Rate limiting (if enabled)
|
// 2. Rate limiting (if enabled)
|
||||||
state
|
state
|
||||||
.rate_limiter
|
.rate_limiter
|
||||||
.check_limit(endpoint, rate_limit)
|
.check("default", endpoint)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
HttpResponse::TooManyRequests().json(json!({
|
HttpResponse::TooManyRequests().json(json!({
|
||||||
"error": format!("Rate limit exceeded: {}", e)
|
"error": format!("Rate limit exceeded: {}", e.reason())
|
||||||
}))
|
}))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ pub struct RankedResult {
|
|||||||
/// GET /memory/ranking/profiles
|
/// GET /memory/ranking/profiles
|
||||||
pub async fn get_ranking_profiles(req: HttpRequest) -> HttpResponse {
|
pub async fn get_ranking_profiles(req: HttpRequest) -> HttpResponse {
|
||||||
// Verify auth
|
// Verify auth
|
||||||
if let Err(e) = AuthGuard::extract_token(&req) {
|
if let Err(e) = AuthGuard::extract_token(req.headers().get("Authorization").and_then(|v| v.to_str().ok()).unwrap_or("")) {
|
||||||
return HttpResponse::Unauthorized().json(json!({
|
return HttpResponse::Unauthorized().json(json!({
|
||||||
"error": e.to_string()
|
"error": e.to_string()
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ pub async fn rebuild(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
// Verify auth
|
// Verify auth
|
||||||
if let Err(e) = AuthGuard::extract_token(&req) {
|
if let Err(e) = AuthGuard::extract_token(req.headers().get("Authorization").and_then(|v| v.to_str().ok()).unwrap_or("")) {
|
||||||
return HttpResponse::Unauthorized().json(json!({
|
return HttpResponse::Unauthorized().json(json!({
|
||||||
"error": e.to_string()
|
"error": e.to_string()
|
||||||
}));
|
}));
|
||||||
@@ -155,7 +155,7 @@ pub async fn rebuild_status(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
// Verify auth
|
// Verify auth
|
||||||
if let Err(e) = AuthGuard::extract_token(&req) {
|
if let Err(e) = AuthGuard::extract_token(req.headers().get("Authorization").and_then(|v| v.to_str().ok()).unwrap_or("")) {
|
||||||
return HttpResponse::Unauthorized().json(json!({
|
return HttpResponse::Unauthorized().json(json!({
|
||||||
"error": e.to_string()
|
"error": e.to_string()
|
||||||
}));
|
}));
|
||||||
@@ -192,11 +192,16 @@ pub async fn rebuild_status(
|
|||||||
async fn compute_state_checksum(pool: &PgPool, project: &str) -> Result<String, sqlx::Error> {
|
async fn compute_state_checksum(pool: &PgPool, project: &str) -> Result<String, sqlx::Error> {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
|
|
||||||
// Entities in order (by id)
|
// Entities in order (by id) - using runtime query to avoid sqlx compile-time check
|
||||||
let entities = sqlx::query!(
|
#[derive(sqlx::FromRow)]
|
||||||
"SELECT id FROM memory_entity WHERE project_id = $1 ORDER BY id",
|
struct IdRow {
|
||||||
project
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let entities: Vec<IdRow> = sqlx::query_as::<_, IdRow>(
|
||||||
|
"SELECT id FROM memory_entity WHERE project_id = $1 ORDER BY id"
|
||||||
)
|
)
|
||||||
|
.bind(project)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -204,16 +209,16 @@ async fn compute_state_checksum(pool: &PgPool, project: &str) -> Result<String,
|
|||||||
hasher.update(row.id.as_bytes());
|
hasher.update(row.id.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edges in order (by id)
|
// Edges in order (by id) - using runtime query to avoid sqlx compile-time check
|
||||||
let edges = sqlx::query!(
|
let edges: Vec<IdRow> = sqlx::query_as::<_, IdRow>(
|
||||||
"SELECT id FROM memory_edge WHERE project_id = $1 ORDER BY id",
|
"SELECT id FROM memory_edge WHERE project_id = $1 ORDER BY id"
|
||||||
project
|
|
||||||
)
|
)
|
||||||
|
.bind(project)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for row in &edges {
|
for row in &edges {
|
||||||
hasher.update(row.id.to_string().as_bytes());
|
hasher.update(row.id.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(format!("{:x}", hasher.finalize()))
|
Ok(format!("{:x}", hasher.finalize()))
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ pub fn internal_error(error: &str) -> HttpResponse {
|
|||||||
HttpResponse::InternalServerError().json(json!({ "error": error }))
|
HttpResponse::InternalServerError().json(json!({ "error": error }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build an unauthorized response (401)
|
||||||
|
pub fn unauthorized(error: &str) -> HttpResponse {
|
||||||
|
HttpResponse::Unauthorized().json(json!({ "error": error }))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ pub async fn search_entities_handler(
|
|||||||
body.query, body.entity_type, body.start_time, body.end_time);
|
body.query, body.entity_type, body.start_time, body.end_time);
|
||||||
|
|
||||||
// 3. Embed query
|
// 3. Embed query
|
||||||
let query_embedding = match state.embeddings.embed_text(&body.query).await {
|
let query_embedding = match state.embeddings.embed_one(&body.query).await {
|
||||||
Ok(emb) => emb,
|
Ok(emb) => emb.to_vec(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Embedding failed: {}", e);
|
error!("Embedding failed: {}", e);
|
||||||
return crate::handlers::response_builder::internal_error(
|
return crate::handlers::response_builder::internal_error(
|
||||||
@@ -278,8 +278,8 @@ pub async fn search_edges_handler(
|
|||||||
body.query, body.relation_type, body.start_time, body.end_time);
|
body.query, body.relation_type, body.start_time, body.end_time);
|
||||||
|
|
||||||
// 3. Embed query
|
// 3. Embed query
|
||||||
let query_embedding = match state.embeddings.embed_text(&body.query).await {
|
let query_embedding = match state.embeddings.embed_one(&body.query).await {
|
||||||
Ok(emb) => emb,
|
Ok(emb) => emb.to_vec(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Embedding failed: {}", e);
|
error!("Embedding failed: {}", e);
|
||||||
return crate::handlers::response_builder::internal_error(
|
return crate::handlers::response_builder::internal_error(
|
||||||
@@ -361,8 +361,8 @@ pub async fn hybrid_search_handler(
|
|||||||
body.query, body.semantic_weight, body.lexical_weight);
|
body.query, body.semantic_weight, body.lexical_weight);
|
||||||
|
|
||||||
// 3. Embed query
|
// 3. Embed query
|
||||||
let query_embedding = match state.embeddings.embed_text(&body.query).await {
|
let query_embedding = match state.embeddings.embed_one(&body.query).await {
|
||||||
Ok(emb) => emb,
|
Ok(emb) => emb.to_vec(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Embedding failed: {}", e);
|
error!("Embedding failed: {}", e);
|
||||||
return crate::handlers::response_builder::internal_error(
|
return crate::handlers::response_builder::internal_error(
|
||||||
|
|||||||
@@ -517,11 +517,12 @@ pub async fn reasoning_paths_handler(
|
|||||||
let elapsed = start_time.elapsed().as_millis();
|
let elapsed = start_time.elapsed().as_millis();
|
||||||
info!("Paths: {} found in {}ms", paths.len(), elapsed);
|
info!("Paths: {} found in {}ms", paths.len(), elapsed);
|
||||||
|
|
||||||
|
let path_count = paths.len();
|
||||||
crate::handlers::response_builder::success_response(ReasoningPathsResponse {
|
crate::handlers::response_builder::success_response(ReasoningPathsResponse {
|
||||||
source_id: body.source_id.clone(),
|
source_id: body.source_id.clone(),
|
||||||
target_id: body.target_id.clone(),
|
target_id: body.target_id.clone(),
|
||||||
paths,
|
paths,
|
||||||
path_count: paths.len(),
|
path_count,
|
||||||
process_time_ms: elapsed,
|
process_time_ms: elapsed,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ pub async fn unified_query_handler(
|
|||||||
body.search_type, body.query, body.entity_type, body.relation_type);
|
body.search_type, body.query, body.entity_type, body.relation_type);
|
||||||
|
|
||||||
// 3. Embed query once (reused for all search types)
|
// 3. Embed query once (reused for all search types)
|
||||||
let query_embedding = match state.embeddings.embed_text(&body.query).await {
|
let query_embedding = match state.embeddings.embed_one(&body.query).await {
|
||||||
Ok(emb) => emb,
|
Ok(emb) => emb.to_vec(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Embedding failed: {}", e);
|
error!("Embedding failed: {}", e);
|
||||||
return crate::handlers::response_builder::internal_error(
|
return crate::handlers::response_builder::internal_error(
|
||||||
|
|||||||
@@ -158,12 +158,12 @@ pub async fn unified_synthesis_handler(
|
|||||||
// Entity Linking
|
// Entity Linking
|
||||||
if body.link_entities {
|
if body.link_entities {
|
||||||
let linker = EntityLinker::new(state.pool.clone());
|
let linker = EntityLinker::new(state.pool.clone());
|
||||||
match linker.link_entities(&body.content) {
|
match linker.link_mentions(&body.content, &body.project).await {
|
||||||
Ok(links) => {
|
Ok((links, _unlinked)) => {
|
||||||
let alias_count = links.iter().filter(|l| l.confidence > 0.85).count();
|
let alias_count = links.iter().filter(|l| l.confidence > 0.85).count();
|
||||||
entity_linking = Some(EntityLinkingResult {
|
entity_linking = Some(EntityLinkingResult {
|
||||||
mention_links: links.iter().map(|l| MentionLinkResponse {
|
mention_links: links.iter().map(|l| MentionLinkResponse {
|
||||||
mention: l.mention.clone(),
|
mention: l.mention_text.clone(),
|
||||||
entity_id: l.entity_id.clone(),
|
entity_id: l.entity_id.clone(),
|
||||||
confidence: l.confidence,
|
confidence: l.confidence,
|
||||||
}).collect(),
|
}).collect(),
|
||||||
@@ -179,14 +179,14 @@ pub async fn unified_synthesis_handler(
|
|||||||
|
|
||||||
// Inference
|
// Inference
|
||||||
if body.infer_facts {
|
if body.infer_facts {
|
||||||
let engine = InferenceEngine::new(state.pool.clone());
|
let engine = InferenceEngine::new(state.pool.clone(), vec![]);
|
||||||
match engine.infer_facts(&body.content, 5, 0.6, &body.project) {
|
match engine.infer_facts(&body.project, &body.content, 5).await {
|
||||||
Ok(facts) => {
|
Ok(facts) => {
|
||||||
inference = Some(InferenceResult {
|
inference = Some(InferenceResult {
|
||||||
inferred_facts: facts.iter().map(|f| InferredFactResponse {
|
inferred_facts: facts.iter().map(|f| InferredFactResponse {
|
||||||
source: f.source.clone(),
|
source: f.source_id.clone(),
|
||||||
relation: f.relation.clone(),
|
relation: f.relation_type.clone(),
|
||||||
target: f.target.clone(),
|
target: f.target_id.clone(),
|
||||||
confidence: f.confidence,
|
confidence: f.confidence,
|
||||||
}).collect(),
|
}).collect(),
|
||||||
fact_count: facts.len(),
|
fact_count: facts.len(),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub async fn get_entity_versions(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
// Verify auth
|
// Verify auth
|
||||||
if let Err(e) = AuthGuard::extract_token(&req) {
|
if let Err(e) = AuthGuard::extract_token(req.headers().get("Authorization").and_then(|v| v.to_str().ok()).unwrap_or("")) {
|
||||||
return HttpResponse::Unauthorized().json(json!({
|
return HttpResponse::Unauthorized().json(json!({
|
||||||
"error": e.to_string()
|
"error": e.to_string()
|
||||||
}));
|
}));
|
||||||
@@ -46,7 +46,7 @@ pub async fn get_entity_version(
|
|||||||
path: web::Path<(String, i32)>,
|
path: web::Path<(String, i32)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
if let Err(e) = AuthGuard::extract_token(&req) {
|
if let Err(e) = AuthGuard::extract_token(req.headers().get("Authorization").and_then(|v| v.to_str().ok()).unwrap_or("")) {
|
||||||
return HttpResponse::Unauthorized().json(json!({
|
return HttpResponse::Unauthorized().json(json!({
|
||||||
"error": e.to_string()
|
"error": e.to_string()
|
||||||
}));
|
}));
|
||||||
@@ -80,7 +80,7 @@ pub async fn get_entity_diff(
|
|||||||
query: web::Query<DiffQuery>,
|
query: web::Query<DiffQuery>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
if let Err(e) = AuthGuard::extract_token(&req) {
|
if let Err(e) = AuthGuard::extract_token(req.headers().get("Authorization").and_then(|v| v.to_str().ok()).unwrap_or("")) {
|
||||||
return HttpResponse::Unauthorized().json(json!({
|
return HttpResponse::Unauthorized().json(json!({
|
||||||
"error": e.to_string()
|
"error": e.to_string()
|
||||||
}));
|
}));
|
||||||
@@ -120,7 +120,7 @@ pub async fn get_entity_at_time(
|
|||||||
query: web::Query<TimeQuery>,
|
query: web::Query<TimeQuery>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
if let Err(e) = AuthGuard::extract_token(&req) {
|
if let Err(e) = AuthGuard::extract_token(req.headers().get("Authorization").and_then(|v| v.to_str().ok()).unwrap_or("")) {
|
||||||
return HttpResponse::Unauthorized().json(json!({
|
return HttpResponse::Unauthorized().json(json!({
|
||||||
"error": e.to_string()
|
"error": e.to_string()
|
||||||
}));
|
}));
|
||||||
@@ -164,7 +164,7 @@ pub async fn get_edge_versions(
|
|||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
if let Err(e) = AuthGuard::extract_token(&req) {
|
if let Err(e) = AuthGuard::extract_token(req.headers().get("Authorization").and_then(|v| v.to_str().ok()).unwrap_or("")) {
|
||||||
return HttpResponse::Unauthorized().json(json!({
|
return HttpResponse::Unauthorized().json(json!({
|
||||||
"error": e.to_string()
|
"error": e.to_string()
|
||||||
}));
|
}));
|
||||||
@@ -196,7 +196,7 @@ pub async fn get_edge_diff(
|
|||||||
query: web::Query<DiffQuery>,
|
query: web::Query<DiffQuery>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
if let Err(e) = AuthGuard::extract_token(&req) {
|
if let Err(e) = AuthGuard::extract_token(req.headers().get("Authorization").and_then(|v| v.to_str().ok()).unwrap_or("")) {
|
||||||
return HttpResponse::Unauthorized().json(json!({
|
return HttpResponse::Unauthorized().json(json!({
|
||||||
"error": e.to_string()
|
"error": e.to_string()
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -145,13 +145,15 @@ pub async fn visualize_stream_handler(
|
|||||||
match execute_streaming_visualization(&state, req_body).await {
|
match execute_streaming_visualization(&state, req_body).await {
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
for event in events {
|
for event in events {
|
||||||
yield format_sse_event(event);
|
let data = format_sse_event(event);
|
||||||
|
yield Ok::<actix_web::web::Bytes, actix_web::Error>(actix_web::web::Bytes::from(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
yield format_sse_event(VisualizeEvent::Error {
|
let data = format_sse_event(VisualizeEvent::Error {
|
||||||
message: e,
|
message: e,
|
||||||
});
|
});
|
||||||
|
yield Ok::<actix_web::web::Bytes, actix_web::Error>(actix_web::web::Bytes::from(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -161,7 +163,7 @@ pub async fn visualize_stream_handler(
|
|||||||
.insert_header(("Cache-Control", "no-cache"))
|
.insert_header(("Cache-Control", "no-cache"))
|
||||||
.insert_header(("Connection", "keep-alive"))
|
.insert_header(("Connection", "keep-alive"))
|
||||||
.insert_header(("Transfer-Encoding", "chunked"))
|
.insert_header(("Transfer-Encoding", "chunked"))
|
||||||
.streaming_body(Box::pin(stream))
|
.streaming(Box::pin(stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute streaming visualization (generates events)
|
/// Execute streaming visualization (generates events)
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ use crate::gateway_queue_adapter::GatewayQueueAdapter;
|
|||||||
use crate::queue_worker::{QueueWorker, QueueWorkerConfig};
|
use crate::queue_worker::{QueueWorker, QueueWorkerConfig};
|
||||||
use crate::queue_adapter::QueueAdapter;
|
use crate::queue_adapter::QueueAdapter;
|
||||||
use crate::rbac::{AccessGuard, Claims as RbacClaims, builtin_role_provider, ResourceMeta, ResourceType, Verb, Visibility};
|
use crate::rbac::{AccessGuard, Claims as RbacClaims, builtin_role_provider, ResourceMeta, ResourceType, Verb, Visibility};
|
||||||
use crate::handlers::{QueryParams, QueryParamsError, SearchMethod, build_search_response, LearnParams, LearnParamsError, build_learn_response};
|
use crate::handlers::{
|
||||||
|
QueryParams, QueryParamsError, SearchMethod, build_search_response,
|
||||||
|
LearnParams, LearnParamsError, build_learn_response,
|
||||||
|
visualize_handler, visualize_stream_handler, compact_handler
|
||||||
|
};
|
||||||
|
|
||||||
/// Server state with database and workers
|
/// Server state with database and workers
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
@@ -46,13 +50,29 @@ pub struct AppState {
|
|||||||
pub enum AuthMode {
|
pub enum AuthMode {
|
||||||
Jwt, // Validate JWT from Authentik
|
Jwt, // Validate JWT from Authentik
|
||||||
ApiKey, // Fallback to static API key
|
ApiKey, // Fallback to static API key
|
||||||
|
None, // No auth (testing only)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auth extractor — validates JWT or fallback to apikey
|
/// Auth extractor — validates JWT, apikey, or disabled
|
||||||
async fn validate_auth(req: &HttpRequest, state: &AppState) -> Result<(JwtClaims, String), HttpResponse> {
|
async fn validate_auth(req: &HttpRequest, state: &AppState) -> Result<(JwtClaims, String), HttpResponse> {
|
||||||
match state.auth_mode {
|
match state.auth_mode {
|
||||||
AuthMode::Jwt => validate_jwt_token(req, state).await,
|
AuthMode::Jwt => validate_jwt_token(req, state).await,
|
||||||
AuthMode::ApiKey => validate_apikey(req, state),
|
AuthMode::ApiKey => validate_apikey(req, state),
|
||||||
|
AuthMode::None => {
|
||||||
|
tracing::warn!("Auth disabled - returning synthetic claims");
|
||||||
|
let claims = JwtClaims {
|
||||||
|
sub: "test-user".to_string(),
|
||||||
|
iss: "test".to_string(),
|
||||||
|
aud: "memory".to_string(),
|
||||||
|
exp: i64::MAX,
|
||||||
|
iat: chrono::Utc::now().timestamp(),
|
||||||
|
nbf: None,
|
||||||
|
permissions: Some(vec!["memory:write".to_string(), "memory:read".to_string()]),
|
||||||
|
groups: Some(vec!["test".to_string()]),
|
||||||
|
roles: None,
|
||||||
|
};
|
||||||
|
Ok((claims, "synthetic-token".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +272,7 @@ pub async fn start_server(port: u16, api_key: String, database_url: &str) -> Res
|
|||||||
let auth_mode = match auth_mode.as_str() {
|
let auth_mode = match auth_mode.as_str() {
|
||||||
"jwt" => AuthMode::Jwt,
|
"jwt" => AuthMode::Jwt,
|
||||||
"apikey" => AuthMode::ApiKey,
|
"apikey" => AuthMode::ApiKey,
|
||||||
|
"none" => AuthMode::None,
|
||||||
_ => {
|
_ => {
|
||||||
tracing::warn!("Unknown auth mode: {}, defaulting to apikey", auth_mode);
|
tracing::warn!("Unknown auth mode: {}, defaulting to apikey", auth_mode);
|
||||||
AuthMode::ApiKey
|
AuthMode::ApiKey
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod endpoints;
|
pub mod endpoints;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
pub mod http_server;
|
pub mod http_server;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
|
pub mod auth;
|
||||||
pub mod ingest_worker;
|
pub mod ingest_worker;
|
||||||
pub mod query_worker;
|
pub mod query_worker;
|
||||||
pub mod rate_limiter;
|
pub mod rate_limiter;
|
||||||
@@ -31,7 +31,7 @@ pub mod federation;
|
|||||||
pub mod query_router;
|
pub mod query_router;
|
||||||
pub mod full_pipeline;
|
pub mod full_pipeline;
|
||||||
pub mod authorized_pipeline;
|
pub mod authorized_pipeline;
|
||||||
pub mod ingest_with_persistence;
|
// pub mod ingest_with_persistence; // TODO: Fix db_repo integration
|
||||||
pub mod auth_middleware;
|
pub mod auth_middleware;
|
||||||
pub mod compaction;
|
pub mod compaction;
|
||||||
pub mod compaction_executor;
|
pub mod compaction_executor;
|
||||||
@@ -39,6 +39,7 @@ pub mod agent;
|
|||||||
pub mod parallel_dual_write;
|
pub mod parallel_dual_write;
|
||||||
|
|
||||||
pub use endpoints::{IngestQueue, IngestRequest, JobStatus};
|
pub use endpoints::{IngestQueue, IngestRequest, JobStatus};
|
||||||
|
pub use http_server::{AppState, AuthMode};
|
||||||
pub use ingest_worker::IngestWorker;
|
pub use ingest_worker::IngestWorker;
|
||||||
pub use query_worker::QueryWorker;
|
pub use query_worker::QueryWorker;
|
||||||
pub use hybrid_retrieval::{HybridRetriever, RetrievalRoute, WikiScopedFilter, RankedCandidate};
|
pub use hybrid_retrieval::{HybridRetriever, RetrievalRoute, WikiScopedFilter, RankedCandidate};
|
||||||
|
|||||||
@@ -448,10 +448,8 @@ async fn cmd_learn(
|
|||||||
all_files.sort();
|
all_files.sort();
|
||||||
println!("Found {} markdown files", all_files.len());
|
println!("Found {} markdown files", all_files.len());
|
||||||
|
|
||||||
// Load configuration from environment (set by K8s ConfigMap)
|
|
||||||
let config = mem_cli::config::ServiceConfig::from_env_dev();
|
|
||||||
let api_url = std::env::var("MEM_API_URL")
|
let api_url = std::env::var("MEM_API_URL")
|
||||||
.unwrap_or_else(|_| config.memory_service_addr.clone());
|
.unwrap_or_else(|_| "http://localhost:8080".to_string());
|
||||||
let api_token = std::env::var("MEM_API_TOKEN").ok();
|
let api_token = std::env::var("MEM_API_TOKEN").ok();
|
||||||
let http = reqwest::Client::builder()
|
let http = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(120))
|
.timeout(std::time::Duration::from_secs(120))
|
||||||
|
|||||||
@@ -116,13 +116,13 @@ impl ParallelDualWriteIndexer {
|
|||||||
|
|
||||||
// Spawn background task (non-blocking)
|
// Spawn background task (non-blocking)
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let result = opensearch.index_chunk(
|
let result = opensearch.index_document(
|
||||||
&chunk_id,
|
&chunk_id,
|
||||||
&chunk.content,
|
&chunk.content,
|
||||||
&chunk.source,
|
&chunk.source,
|
||||||
&chunk.project,
|
|
||||||
&chunk.level,
|
&chunk.level,
|
||||||
&chunk.breadcrumb.join(" > "),
|
chunk.breadcrumb.clone(),
|
||||||
|
"", // jwt_token - not available in background task
|
||||||
).await;
|
).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
@@ -139,8 +139,8 @@ impl ParallelDualWriteIndexer {
|
|||||||
&self,
|
&self,
|
||||||
chunks: Vec<(&IndexableChunk, Vec<f32>)>,
|
chunks: Vec<(&IndexableChunk, Vec<f32>)>,
|
||||||
) -> Vec<DualWriteResult> {
|
) -> Vec<DualWriteResult> {
|
||||||
let futures = chunks.into_iter().map(|(chunk, embedding)| {
|
let futures = chunks.into_iter().map(|(chunk, embedding)| async move {
|
||||||
self.index_parallel(chunk, &embedding)
|
self.index_parallel(chunk, &embedding).await
|
||||||
});
|
});
|
||||||
|
|
||||||
futures::future::join_all(futures)
|
futures::future::join_all(futures)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres, Row};
|
||||||
|
|
||||||
/// A node in the traversal result
|
/// A node in the traversal result
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -196,6 +196,7 @@ impl BfsGraphTraversal {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let edge_count = edges.len();
|
||||||
Ok(GraphData {
|
Ok(GraphData {
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
@@ -203,7 +204,7 @@ impl BfsGraphTraversal {
|
|||||||
requested_depth: config.max_depth,
|
requested_depth: config.max_depth,
|
||||||
max_depth_reached: max_depth,
|
max_depth_reached: max_depth,
|
||||||
node_count: visited.len(),
|
node_count: visited.len(),
|
||||||
edge_count: edges.len(),
|
edge_count,
|
||||||
depth_breakdown,
|
depth_breakdown,
|
||||||
traversal_time_ms: start_time.elapsed().as_millis() as u64,
|
traversal_time_ms: start_time.elapsed().as_millis() as u64,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -185,9 +185,9 @@ impl CommunityDetector {
|
|||||||
|
|
||||||
communities_vec.push(Community {
|
communities_vec.push(Community {
|
||||||
id: comm_id,
|
id: comm_id,
|
||||||
|
size: members.len(),
|
||||||
entity_ids: members.into_iter().collect(),
|
entity_ids: members.into_iter().collect(),
|
||||||
entity_names,
|
entity_names,
|
||||||
size: members.len(),
|
|
||||||
modularity_contribution: modularity_contrib,
|
modularity_contribution: modularity_contrib,
|
||||||
average_strength: strength,
|
average_strength: strength,
|
||||||
density,
|
density,
|
||||||
@@ -196,9 +196,9 @@ impl CommunityDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Calculate total modularity
|
// 5. Calculate total modularity
|
||||||
let total_modularity = communities_vec
|
let total_modularity: f64 = communities_vec
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.modularity_contribution)
|
.map(|c| c.modularity_contribution as f64)
|
||||||
.sum();
|
.sum();
|
||||||
|
|
||||||
let average_community_size = if communities_vec.is_empty() {
|
let average_community_size = if communities_vec.is_empty() {
|
||||||
@@ -210,9 +210,9 @@ impl CommunityDetector {
|
|||||||
let result = CommunityDetectionResult {
|
let result = CommunityDetectionResult {
|
||||||
entity_count: entities.len(),
|
entity_count: entities.len(),
|
||||||
edge_count: edges.len(),
|
edge_count: edges.len(),
|
||||||
communities: communities_vec,
|
|
||||||
community_count: communities_vec.len(),
|
community_count: communities_vec.len(),
|
||||||
total_modularity: total_modularity.max(-1.0).min(1.0),
|
communities: communities_vec,
|
||||||
|
total_modularity: total_modularity.max(-1.0).min(1.0) as f32,
|
||||||
average_community_size,
|
average_community_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -202,10 +202,10 @@ impl CommunityMetricsCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Rank communities by metric
|
/// Rank communities by metric
|
||||||
pub fn rank_by_metric(
|
pub fn rank_by_metric<'a>(
|
||||||
metrics: &[CommunityMetrics],
|
metrics: &'a [CommunityMetrics],
|
||||||
metric: &str,
|
metric: &str,
|
||||||
) -> Vec<&CommunityMetrics> {
|
) -> Vec<&'a CommunityMetrics> {
|
||||||
let mut sorted = metrics.iter().collect::<Vec<_>>();
|
let mut sorted = metrics.iter().collect::<Vec<_>>();
|
||||||
|
|
||||||
match metric {
|
match metric {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Enables multi-dimensional filtering across entities and edges.
|
//! Enables multi-dimensional filtering across entities and edges.
|
||||||
//! Supports entity types, relation types, date ranges, confidence levels, and more.
|
//! Supports entity types, relation types, date ranges, confidence levels, and more.
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Timelike, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -42,7 +42,7 @@ pub struct AvailableFacets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Facet filters for a query
|
/// Facet filters for a query
|
||||||
#[derive(Debug, Clone, Default, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct FacetFilters {
|
pub struct FacetFilters {
|
||||||
/// Filter by entity types (OR within facet, AND across facets)
|
/// Filter by entity types (OR within facet, AND across facets)
|
||||||
pub entity_types: Option<Vec<String>>,
|
pub entity_types: Option<Vec<String>>,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use super::bfs_graph_traversal::{GraphData, TraversalNode, TraversalEdge};
|
use super::bfs_graph_traversal::{GraphData, TraversalNode, TraversalEdge};
|
||||||
|
|
||||||
/// 2D position (X, Y coordinates)
|
/// 2D position (X, Y coordinates)
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||||
pub struct Position {
|
pub struct Position {
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
pub y: f32,
|
pub y: f32,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
//! confidence propagation through reasoning chains.
|
//! confidence propagation through reasoning chains.
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::future::Future;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
@@ -293,18 +295,19 @@ impl InferenceEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// DFS to find all paths
|
/// DFS to find all paths
|
||||||
async fn dfs_paths(
|
fn dfs_paths<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
current: &str,
|
current: &'a str,
|
||||||
target: &str,
|
target: &'a str,
|
||||||
project_id: &str,
|
project_id: &'a str,
|
||||||
remaining_hops: usize,
|
remaining_hops: usize,
|
||||||
path: &mut Vec<String>,
|
path: &'a mut Vec<String>,
|
||||||
relations: &mut Vec<String>,
|
relations: &'a mut Vec<String>,
|
||||||
confidences: &mut Vec<f32>,
|
confidences: &'a mut Vec<f32>,
|
||||||
visited: &mut HashSet<String>,
|
visited: &'a mut HashSet<String>,
|
||||||
results: &mut Vec<ReasoningPath>,
|
results: &'a mut Vec<ReasoningPath>,
|
||||||
) -> Result<(), String> {
|
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
|
||||||
|
Box::pin(async move {
|
||||||
if remaining_hops == 0 {
|
if remaining_hops == 0 {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -350,6 +353,7 @@ impl InferenceEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}) // Box::pin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::future::Future;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
/// A single path through the graph
|
/// A single path through the graph
|
||||||
@@ -123,13 +125,14 @@ impl PathFinder {
|
|||||||
info!("Found shortest path: {} → {} (distance: {})",
|
info!("Found shortest path: {} → {} (distance: {})",
|
||||||
source_id, target_id, final_entities.len() - 1);
|
source_id, target_id, final_entities.len() - 1);
|
||||||
|
|
||||||
|
let distance = final_entities.len() - 1;
|
||||||
return Ok(Some(Path {
|
return Ok(Some(Path {
|
||||||
source_id: source_id.to_string(),
|
source_id: source_id.to_string(),
|
||||||
target_id: target_id.to_string(),
|
target_id: target_id.to_string(),
|
||||||
entity_ids: final_entities,
|
entity_ids: final_entities,
|
||||||
entity_names: vec![], // Could fetch from DB if needed
|
entity_names: vec![], // Could fetch from DB if needed
|
||||||
relation_types: final_relations,
|
relation_types: final_relations,
|
||||||
distance: final_entities.len() - 1,
|
distance,
|
||||||
total_confidence: final_confidence.max(0.0).min(1.0),
|
total_confidence: final_confidence.max(0.0).min(1.0),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -293,19 +296,20 @@ impl PathFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// DFS helper for finding all paths
|
/// DFS helper for finding all paths
|
||||||
async fn dfs_paths(
|
fn dfs_paths<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
source_id: &str,
|
source_id: &'a str,
|
||||||
target_id: &str,
|
target_id: &'a str,
|
||||||
current_path: Vec<String>,
|
current_path: Vec<String>,
|
||||||
relations_path: Vec<String>,
|
relations_path: Vec<String>,
|
||||||
confidence: f32,
|
confidence: f32,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
max_depth: usize,
|
max_depth: usize,
|
||||||
paths_found: &mut Vec<Path>,
|
paths_found: &'a mut Vec<Path>,
|
||||||
visited: &mut HashSet<String>,
|
visited: &'a mut HashSet<String>,
|
||||||
max_paths: usize,
|
max_paths: usize,
|
||||||
) -> Result<(), String> {
|
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
|
||||||
|
Box::pin(async move {
|
||||||
if paths_found.len() >= max_paths {
|
if paths_found.len() >= max_paths {
|
||||||
return Ok(()); // Found enough paths
|
return Ok(()); // Found enough paths
|
||||||
}
|
}
|
||||||
@@ -328,13 +332,14 @@ impl PathFinder {
|
|||||||
|
|
||||||
let final_confidence = confidence * edge.confidence;
|
let final_confidence = confidence * edge.confidence;
|
||||||
|
|
||||||
|
let distance = final_path.len() - 1;
|
||||||
paths_found.push(Path {
|
paths_found.push(Path {
|
||||||
source_id: source_id.to_string(),
|
source_id: source_id.to_string(),
|
||||||
target_id: target_id.to_string(),
|
target_id: target_id.to_string(),
|
||||||
entity_ids: final_path,
|
entity_ids: final_path,
|
||||||
entity_names: vec![],
|
entity_names: vec![],
|
||||||
relation_types: final_relations,
|
relation_types: final_relations,
|
||||||
distance: final_path.len() - 1,
|
distance,
|
||||||
total_confidence: final_confidence.max(0.0).min(1.0),
|
total_confidence: final_confidence.max(0.0).min(1.0),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -370,6 +375,7 @@ impl PathFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}) // Box::pin
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch direct neighbors of an entity
|
/// Fetch direct neighbors of an entity
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ impl SemanticRetriever {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Database error: {}", e))?;
|
.map_err(|e| format!("Database error: {}", e))?;
|
||||||
|
|
||||||
let entities = results
|
let entities: Vec<_> = results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(id, name, entity_type, score, metadata)| EntityResult {
|
.map(|(id, name, entity_type, score, metadata)| EntityResult {
|
||||||
id,
|
id,
|
||||||
@@ -213,7 +213,7 @@ impl SemanticRetriever {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Database error: {}", e))?;
|
.map_err(|e| format!("Database error: {}", e))?;
|
||||||
|
|
||||||
let edges = results
|
let edges: Vec<_> = results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(id, src_id, tgt_id, src_name, tgt_name, rel_type, fact, score, conf)| {
|
.map(|(id, src_id, tgt_id, src_name, tgt_name, rel_type, fact, score, conf)| {
|
||||||
EdgeResult {
|
EdgeResult {
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ impl Summarizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Split text into sentences
|
/// Split text into sentences
|
||||||
fn split_sentences(&self, text: &str) -> Vec<&str> {
|
fn split_sentences<'a>(&self, text: &'a str) -> Vec<&'a str> {
|
||||||
text.split('.').map(|s| s.trim()).filter(|s| !s.is_empty()).collect()
|
text.split('.').map(|s| s.trim()).filter(|s| !s.is_empty()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +331,7 @@ impl Summarizer {
|
|||||||
|
|
||||||
let overlap = entities1
|
let overlap = entities1
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|e| entities2.contains(e))
|
.filter(|e| entities2.contains(*e))
|
||||||
.count();
|
.count();
|
||||||
coherence += overlap as f32 / (entities1.len().max(entities2.len()) as f32).max(1.0);
|
coherence += overlap as f32 / (entities1.len().max(entities2.len()) as f32).max(1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ impl QueryRouter {
|
|||||||
let latency_ms = start.elapsed().as_millis() as u64;
|
let latency_ms = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
// Phase 8: Answer Validation (confidence scoring)
|
// Phase 8: Answer Validation (confidence scoring)
|
||||||
use crate::answer_validator::{AnswerValidator, AnswerValidationConfig, ConfidenceSignals};
|
use crate::query::answer_validator::{AnswerValidator, AnswerValidationConfig, ConfidenceSignals};
|
||||||
let validator = AnswerValidator::new(AnswerValidationConfig::default());
|
let validator = AnswerValidator::new(AnswerValidationConfig::default());
|
||||||
let avg_score = selected_chunks.iter().map(|c| c.final_score).sum::<f32>()
|
let avg_score = selected_chunks.iter().map(|c| c.final_score).sum::<f32>()
|
||||||
/ (selected_chunks.len() as f32).max(1.0);
|
/ (selected_chunks.len() as f32).max(1.0);
|
||||||
@@ -245,6 +245,8 @@ impl QueryRouter {
|
|||||||
prefilter_size,
|
prefilter_size,
|
||||||
metrics,
|
metrics,
|
||||||
latency_ms,
|
latency_ms,
|
||||||
|
confidence_score: 1.0,
|
||||||
|
is_valid: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+52
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n version_num,\n operation,\n snapshot,\n changed_at,\n changed_by,\n COALESCE(fields_changed, '{}') as \"fields_changed!\"\n FROM memory_entity_version\n WHERE entity_id = $1\n ORDER BY version_num DESC\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "version_num",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "operation",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "snapshot",
|
||||||
|
"type_info": "Jsonb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "changed_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "fields_changed!",
|
||||||
|
"type_info": "TextArray"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "1e81bb729531ca33e4cef21623bcfe4fafb0c1bd435353b205f582bfda8873bc"
|
||||||
|
}
|
||||||
Generated
+52
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n version_num,\n operation,\n snapshot,\n changed_at,\n changed_by,\n COALESCE(fields_changed, '{}') as \"fields_changed!\"\n FROM memory_edge_version\n WHERE edge_id = $1\n ORDER BY version_num DESC\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "version_num",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "operation",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "snapshot",
|
||||||
|
"type_info": "Jsonb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "changed_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "fields_changed!",
|
||||||
|
"type_info": "TextArray"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "62d65d4afc4d292b37de8e5cb59fbd51c602bdc1b437988f54e6c7fe268b9816"
|
||||||
|
}
|
||||||
Generated
+53
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n version_num,\n operation,\n snapshot,\n changed_at,\n changed_by,\n COALESCE(fields_changed, '{}') as \"fields_changed!\"\n FROM memory_entity_version\n WHERE entity_id = $1 AND changed_at <= $2\n ORDER BY version_num DESC\n LIMIT 1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "version_num",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "operation",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "snapshot",
|
||||||
|
"type_info": "Jsonb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "changed_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "fields_changed!",
|
||||||
|
"type_info": "TextArray"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text",
|
||||||
|
"Timestamptz"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "aee5900f5e3d7cbba23729bbf2dd033dcc4cb41f6c851bf447a9238810684d18"
|
||||||
|
}
|
||||||
Generated
+53
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n version_num,\n operation,\n snapshot,\n changed_at,\n changed_by,\n COALESCE(fields_changed, '{}') as \"fields_changed!\"\n FROM memory_entity_version\n WHERE entity_id = $1 AND version_num = $2\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "version_num",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "operation",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "snapshot",
|
||||||
|
"type_info": "Jsonb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "changed_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "fields_changed!",
|
||||||
|
"type_info": "TextArray"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text",
|
||||||
|
"Int4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "c045466e1fe037dbdafea1008f262f4e48f104ea77732aa1d32ecb797f70e71d"
|
||||||
|
}
|
||||||
Generated
+53
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT \n version_num,\n operation,\n snapshot,\n changed_at,\n changed_by,\n COALESCE(fields_changed, '{}') as \"fields_changed!\"\n FROM memory_edge_version\n WHERE edge_id = $1 AND version_num = $2\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "version_num",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "operation",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "snapshot",
|
||||||
|
"type_info": "Jsonb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "changed_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "changed_by",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "fields_changed!",
|
||||||
|
"type_info": "TextArray"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid",
|
||||||
|
"Int4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "ca6872495bc04c6a65531279af8c758637c902dda2cc10366662988c6973ca48"
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
-- Phase 4: Community Detection Schema
|
||||||
|
-- Extends memory_community with label propagation execution and statistics
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 1: Create label propagation run tracking
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS label_propagation_run (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
run_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
algorithm VARCHAR(50) DEFAULT 'label_propagation',
|
||||||
|
max_iterations INT DEFAULT 10,
|
||||||
|
convergence_threshold FLOAT DEFAULT 0.01,
|
||||||
|
iterations_completed INT,
|
||||||
|
converged BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Execution metadata
|
||||||
|
status VARCHAR(20) DEFAULT 'running'
|
||||||
|
CHECK (status IN ('running', 'completed', 'failed')),
|
||||||
|
error_message TEXT,
|
||||||
|
duration_ms INT,
|
||||||
|
|
||||||
|
-- Statistics
|
||||||
|
communities_detected INT,
|
||||||
|
communities_merged INT,
|
||||||
|
communities_split INT,
|
||||||
|
nodes_processed INT,
|
||||||
|
edges_processed INT,
|
||||||
|
|
||||||
|
-- Execution mode
|
||||||
|
dry_run BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
CONSTRAINT chk_iterations_valid CHECK (iterations_completed >= 0 AND iterations_completed <= max_iterations)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_label_prop_run_project
|
||||||
|
ON label_propagation_run(project_id, run_at DESC);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_label_prop_run_status
|
||||||
|
ON label_propagation_run(project_id, status)
|
||||||
|
WHERE status IN ('running', 'failed');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 2: Create community member map
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS community_member_map (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
community_id UUID NOT NULL REFERENCES memory_community(id) ON DELETE CASCADE,
|
||||||
|
entity_id UUID NOT NULL REFERENCES memory_entity(id) ON DELETE CASCADE,
|
||||||
|
label_propagation_run_id UUID REFERENCES label_propagation_run(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Label strength (0-1, higher = stronger membership)
|
||||||
|
label_strength FLOAT DEFAULT 1.0,
|
||||||
|
|
||||||
|
-- Membership tracking
|
||||||
|
is_seed BOOLEAN DEFAULT FALSE,
|
||||||
|
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
left_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Consistency
|
||||||
|
CONSTRAINT uq_community_entity_project UNIQUE (project_id, community_id, entity_id),
|
||||||
|
CONSTRAINT chk_label_strength CHECK (label_strength >= 0 AND label_strength <= 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_community_member_project
|
||||||
|
ON community_member_map(project_id, community_id);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_community_entity_lookup
|
||||||
|
ON community_member_map(entity_id, community_id);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_community_member_strength
|
||||||
|
ON community_member_map(community_id, label_strength DESC)
|
||||||
|
WHERE left_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_community_seeds
|
||||||
|
ON community_member_map(project_id, is_seed)
|
||||||
|
WHERE is_seed = TRUE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 3: Create community statistics table
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS community_statistics (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
community_id UUID NOT NULL UNIQUE REFERENCES memory_community(id) ON DELETE CASCADE,
|
||||||
|
label_propagation_run_id UUID NOT NULL REFERENCES label_propagation_run(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Membership stats
|
||||||
|
member_count INT DEFAULT 0,
|
||||||
|
active_member_count INT DEFAULT 0,
|
||||||
|
seed_member_count INT DEFAULT 0,
|
||||||
|
|
||||||
|
-- Graph structure
|
||||||
|
internal_edge_count INT DEFAULT 0,
|
||||||
|
external_edge_count INT DEFAULT 0,
|
||||||
|
|
||||||
|
-- Cohesion metrics
|
||||||
|
density FLOAT DEFAULT 0.0,
|
||||||
|
modularity FLOAT DEFAULT 0.0,
|
||||||
|
|
||||||
|
-- Edge types within community
|
||||||
|
relation_type_distribution JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Temporal metrics
|
||||||
|
first_entity_created TIMESTAMPTZ,
|
||||||
|
last_entity_accessed TIMESTAMPTZ,
|
||||||
|
avg_entity_age_days FLOAT DEFAULT 0.0,
|
||||||
|
|
||||||
|
-- Quality scores
|
||||||
|
coherence_score FLOAT DEFAULT 0.5,
|
||||||
|
stability_score FLOAT DEFAULT 0.5,
|
||||||
|
significance_score FLOAT DEFAULT 0.5,
|
||||||
|
|
||||||
|
CONSTRAINT chk_stats_nonnegative CHECK (
|
||||||
|
member_count >= 0 AND
|
||||||
|
internal_edge_count >= 0 AND
|
||||||
|
external_edge_count >= 0
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_stats_bounded CHECK (
|
||||||
|
density >= 0 AND density <= 1 AND
|
||||||
|
modularity >= -1 AND modularity <= 1 AND
|
||||||
|
coherence_score >= 0 AND coherence_score <= 1 AND
|
||||||
|
stability_score >= 0 AND stability_score <= 1 AND
|
||||||
|
significance_score >= 0 AND significance_score <= 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_community_stats_project
|
||||||
|
ON community_statistics(project_id, community_id);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_community_stats_run
|
||||||
|
ON community_statistics(label_propagation_run_id);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_community_stats_quality
|
||||||
|
ON community_statistics(project_id, coherence_score DESC, significance_score DESC)
|
||||||
|
WHERE coherence_score > 0.7;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 4: Create community merge history
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS community_merge_history (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
source_community_id UUID NOT NULL REFERENCES memory_community(id) ON DELETE CASCADE,
|
||||||
|
target_community_id UUID NOT NULL REFERENCES memory_community(id) ON DELETE CASCADE,
|
||||||
|
merge_reason VARCHAR(100),
|
||||||
|
merged_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
label_propagation_run_id UUID REFERENCES label_propagation_run(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Rollback capability
|
||||||
|
dry_run BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Statistics before merge
|
||||||
|
source_member_count INT,
|
||||||
|
target_member_count INT,
|
||||||
|
|
||||||
|
-- Impact
|
||||||
|
members_moved INT,
|
||||||
|
edges_reattached INT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_merge_history_project
|
||||||
|
ON community_merge_history(project_id, merged_at DESC);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_merge_history_communities
|
||||||
|
ON community_merge_history(source_community_id, target_community_id);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 5: Add community detection status to memory_community
|
||||||
|
-- ============================================
|
||||||
|
ALTER TABLE memory_community
|
||||||
|
ADD COLUMN IF NOT EXISTS last_detection_run_id UUID REFERENCES label_propagation_run(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS detection_score FLOAT DEFAULT 0.5,
|
||||||
|
ADD COLUMN IF NOT EXISTS is_permanent BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS merge_into_id UUID REFERENCES memory_community(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_community_detection_run
|
||||||
|
ON memory_community(last_detection_run_id, detection_score DESC)
|
||||||
|
WHERE detection_score > 0.7;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 6: Add community-level summary generation tracking
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS community_summary_generation (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
community_id UUID NOT NULL REFERENCES memory_community(id) ON DELETE CASCADE,
|
||||||
|
generated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
generated_by VARCHAR(255),
|
||||||
|
|
||||||
|
-- LLM usage
|
||||||
|
llm_model VARCHAR(100),
|
||||||
|
input_tokens INT,
|
||||||
|
output_tokens INT,
|
||||||
|
cost_usd FLOAT,
|
||||||
|
|
||||||
|
-- Generation method
|
||||||
|
method VARCHAR(50) DEFAULT 'extractive', -- 'extractive' or 'abstractive'
|
||||||
|
|
||||||
|
-- Quality
|
||||||
|
coherence_rating INT CHECK (coherence_rating >= 1 AND coherence_rating <= 5),
|
||||||
|
user_feedback TEXT,
|
||||||
|
|
||||||
|
-- Result
|
||||||
|
summary_text TEXT NOT NULL,
|
||||||
|
summary_embedding VECTOR(768),
|
||||||
|
|
||||||
|
-- Versioning
|
||||||
|
version INT DEFAULT 1,
|
||||||
|
is_latest BOOLEAN DEFAULT TRUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_community_summary_latest
|
||||||
|
ON community_summary_generation(community_id, generated_at DESC)
|
||||||
|
WHERE is_latest = TRUE;
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_community_summary_embedding
|
||||||
|
ON community_summary_generation USING hnsw (summary_embedding vector_cosine_ops)
|
||||||
|
WITH (m = 16, ef_construction = 200)
|
||||||
|
WHERE is_latest = TRUE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ROLLBACK INSTRUCTIONS
|
||||||
|
-- ============================================
|
||||||
|
-- DROP TABLE IF EXISTS community_summary_generation;
|
||||||
|
-- DROP TABLE IF EXISTS community_merge_history;
|
||||||
|
-- DROP TABLE IF EXISTS community_statistics;
|
||||||
|
-- DROP TABLE IF EXISTS community_member_map;
|
||||||
|
-- DROP TABLE IF EXISTS label_propagation_run;
|
||||||
|
-- ALTER TABLE memory_community DROP COLUMN IF EXISTS last_detection_run_id;
|
||||||
|
-- ALTER TABLE memory_community DROP COLUMN IF EXISTS detection_score;
|
||||||
|
-- ALTER TABLE memory_community DROP COLUMN IF EXISTS is_permanent;
|
||||||
|
-- ALTER TABLE memory_community DROP COLUMN IF EXISTS merge_into_id;
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
-- Phase 3: Compaction Schema
|
||||||
|
-- T3.1-T3.4: Deduplication, GC, and dry-run support
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 1: Exact dedup tracking (T3.1)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS exact_dedup_record (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
-- Source and target edges
|
||||||
|
source_edge_id UUID NOT NULL REFERENCES memory_edge(id) ON DELETE CASCADE,
|
||||||
|
target_edge_id UUID NOT NULL REFERENCES memory_edge(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Match criteria (all must match for exact dedup)
|
||||||
|
source_match BOOLEAN NOT NULL,
|
||||||
|
target_match BOOLEAN NOT NULL,
|
||||||
|
relation_match BOOLEAN NOT NULL,
|
||||||
|
fact_match BOOLEAN NOT NULL,
|
||||||
|
|
||||||
|
-- Dedup decision
|
||||||
|
dedup_action VARCHAR(20) DEFAULT 'pending'
|
||||||
|
CHECK (dedup_action IN ('pending', 'merged', 'kept_separate', 'manual_review')),
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
detected_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
processed_at TIMESTAMPTZ,
|
||||||
|
compaction_run_id UUID REFERENCES compaction_log(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Dry-run support
|
||||||
|
dry_run BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
CONSTRAINT chk_unique_edge_pair UNIQUE (source_edge_id, target_edge_id, project_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_exact_dedup_project
|
||||||
|
ON exact_dedup_record(project_id, dedup_action);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_exact_dedup_edges
|
||||||
|
ON exact_dedup_record(source_edge_id, target_edge_id);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_exact_dedup_pending
|
||||||
|
ON exact_dedup_record(project_id, detected_at)
|
||||||
|
WHERE dedup_action = 'pending';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 2: Stale GC tracking (T3.1)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS stale_gc_record (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
-- Entity or edge marked for GC
|
||||||
|
entity_id UUID REFERENCES memory_entity(id) ON DELETE CASCADE,
|
||||||
|
edge_id UUID REFERENCES memory_edge(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Staleness criteria
|
||||||
|
age_days INT NOT NULL,
|
||||||
|
t_invalid_at TIMESTAMPTZ,
|
||||||
|
access_count BIGINT DEFAULT 0,
|
||||||
|
|
||||||
|
-- GC decision
|
||||||
|
gc_action VARCHAR(20) DEFAULT 'pending'
|
||||||
|
CHECK (gc_action IN ('pending', 'deleted', 'archived', 'kept')),
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
detected_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
processed_at TIMESTAMPTZ,
|
||||||
|
compaction_run_id UUID REFERENCES compaction_log(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Dry-run support
|
||||||
|
dry_run BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
CONSTRAINT chk_entity_or_edge CHECK (
|
||||||
|
(entity_id IS NOT NULL AND edge_id IS NULL) OR
|
||||||
|
(entity_id IS NULL AND edge_id IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_stale_gc_project
|
||||||
|
ON stale_gc_record(project_id, gc_action);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_stale_gc_age
|
||||||
|
ON stale_gc_record(project_id, age_days DESC)
|
||||||
|
WHERE gc_action = 'pending';
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_stale_gc_invalid
|
||||||
|
ON stale_gc_record(t_invalid_at)
|
||||||
|
WHERE t_invalid_at IS NOT NULL AND gc_action = 'pending';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 3: Semantic dedup with LLM verification (T3.2)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS semantic_dedup_record (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
-- Source and target edges
|
||||||
|
source_edge_id UUID NOT NULL REFERENCES memory_edge(id) ON DELETE CASCADE,
|
||||||
|
target_edge_id UUID NOT NULL REFERENCES memory_edge(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Pre-filter score (0-1, eliminates 60-70% of candidates)
|
||||||
|
prefilter_score FLOAT NOT NULL,
|
||||||
|
prefilter_passed BOOLEAN NOT NULL,
|
||||||
|
|
||||||
|
-- LLM verification (if prefilter_passed = true)
|
||||||
|
llm_model VARCHAR(100),
|
||||||
|
llm_prompt TEXT,
|
||||||
|
llm_response TEXT,
|
||||||
|
llm_confidence FLOAT,
|
||||||
|
llm_cost_usd FLOAT,
|
||||||
|
|
||||||
|
-- Dedup decision
|
||||||
|
dedup_action VARCHAR(50) DEFAULT 'pending'
|
||||||
|
CHECK (dedup_action IN (
|
||||||
|
'pending', 'auto_merged', 'auto_kept_separate',
|
||||||
|
'manual_review', 'llm_error', 'below_threshold'
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Merge strategy (if auto-merged)
|
||||||
|
merge_strategy VARCHAR(50), -- 'keep_superset', 'keep_newer', 'keep_higher_confidence'
|
||||||
|
merged_edge_id UUID REFERENCES memory_edge(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
detected_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
processed_at TIMESTAMPTZ,
|
||||||
|
compaction_run_id UUID REFERENCES compaction_log(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Dry-run support
|
||||||
|
dry_run BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
CONSTRAINT chk_confidence_valid CHECK (
|
||||||
|
llm_confidence IS NULL OR (llm_confidence >= 0 AND llm_confidence <= 1)
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_prefilter_valid CHECK (prefilter_score >= 0 AND prefilter_score <= 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_semantic_dedup_project
|
||||||
|
ON semantic_dedup_record(project_id, dedup_action);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_semantic_dedup_pending
|
||||||
|
ON semantic_dedup_record(project_id, llm_confidence DESC NULLS LAST)
|
||||||
|
WHERE dedup_action = 'manual_review' OR dedup_action = 'pending';
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_semantic_dedup_edges
|
||||||
|
ON semantic_dedup_record(source_edge_id, target_edge_id);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_semantic_dedup_merged
|
||||||
|
ON semantic_dedup_record(project_id, merged_edge_id)
|
||||||
|
WHERE merged_edge_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 4: Compaction audit trail (T3.3)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS compaction_audit (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
compaction_run_id UUID NOT NULL REFERENCES compaction_log(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Action details
|
||||||
|
action_type VARCHAR(50) NOT NULL, -- 'exact_dedup', 'semantic_dedup', 'stale_gc', etc.
|
||||||
|
source_id UUID,
|
||||||
|
target_id UUID,
|
||||||
|
|
||||||
|
-- Before state
|
||||||
|
before_state JSONB NOT NULL,
|
||||||
|
before_hash VARCHAR(64),
|
||||||
|
|
||||||
|
-- After state
|
||||||
|
after_state JSONB NOT NULL,
|
||||||
|
after_hash VARCHAR(64),
|
||||||
|
|
||||||
|
-- Provenance
|
||||||
|
initiated_by VARCHAR(255),
|
||||||
|
approval_status VARCHAR(50) DEFAULT 'pending'
|
||||||
|
CHECK (approval_status IN ('pending', 'approved', 'rejected', 'auto')),
|
||||||
|
approved_by VARCHAR(255),
|
||||||
|
approval_reason TEXT,
|
||||||
|
|
||||||
|
-- Rollback capability
|
||||||
|
is_reversible BOOLEAN DEFAULT TRUE,
|
||||||
|
reversal_instructions JSONB,
|
||||||
|
|
||||||
|
-- Dry-run tracking
|
||||||
|
dry_run BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Timestamp
|
||||||
|
recorded_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_compaction_audit_run
|
||||||
|
ON compaction_audit(compaction_run_id);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_compaction_audit_project
|
||||||
|
ON compaction_audit(project_id, recorded_at DESC);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_compaction_audit_reversible
|
||||||
|
ON compaction_audit(project_id, recorded_at DESC)
|
||||||
|
WHERE is_reversible = TRUE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 5: Compaction dry-run validation
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS compaction_dryrun_result (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
compaction_run_id UUID NOT NULL REFERENCES compaction_log(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Dry-run metadata
|
||||||
|
started_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Statistics
|
||||||
|
exact_dedup_candidates INT DEFAULT 0,
|
||||||
|
exact_dedup_safe INT DEFAULT 0,
|
||||||
|
|
||||||
|
semantic_dedup_candidates INT DEFAULT 0,
|
||||||
|
semantic_dedup_safe INT DEFAULT 0,
|
||||||
|
semantic_dedup_manual_review INT DEFAULT 0,
|
||||||
|
|
||||||
|
stale_gc_candidates INT DEFAULT 0,
|
||||||
|
stale_gc_safe INT DEFAULT 0,
|
||||||
|
|
||||||
|
-- Predicted impact
|
||||||
|
predicted_space_freed_mb FLOAT DEFAULT 0.0,
|
||||||
|
predicted_edge_count_reduction INT DEFAULT 0,
|
||||||
|
predicted_entity_count_reduction INT DEFAULT 0,
|
||||||
|
|
||||||
|
-- Validation issues found
|
||||||
|
issues_found INT DEFAULT 0,
|
||||||
|
issue_details JSONB DEFAULT '[]',
|
||||||
|
|
||||||
|
-- Decision
|
||||||
|
approval_recommended BOOLEAN DEFAULT FALSE,
|
||||||
|
approval_reason TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_dryrun_project
|
||||||
|
ON compaction_dryrun_result(project_id, completed_at DESC);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_dryrun_run
|
||||||
|
ON compaction_dryrun_result(compaction_run_id);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- STEP 6: Scheduled compaction jobs (T3.4)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS compaction_schedule (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
project_id VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
-- Schedule config
|
||||||
|
cron_expression VARCHAR(100) NOT NULL, -- e.g., "0 2 * * *" for daily at 2 AM UTC
|
||||||
|
timezone VARCHAR(50) DEFAULT 'UTC',
|
||||||
|
|
||||||
|
-- Execution config
|
||||||
|
tier INT DEFAULT 1, -- 1 = exact dedup, 2 = semantic dedup, 3 = both
|
||||||
|
dry_run_first BOOLEAN DEFAULT TRUE,
|
||||||
|
auto_approve_safe_actions BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Resource limits
|
||||||
|
max_execution_time_minutes INT DEFAULT 60,
|
||||||
|
max_llm_cost_usd FLOAT DEFAULT 10.0,
|
||||||
|
|
||||||
|
-- Status
|
||||||
|
enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
last_run_at TIMESTAMPTZ,
|
||||||
|
next_run_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Notifications
|
||||||
|
notify_on_completion BOOLEAN DEFAULT TRUE,
|
||||||
|
notify_emails TEXT[] DEFAULT '{}'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_schedule_project
|
||||||
|
ON compaction_schedule(project_id, enabled)
|
||||||
|
WHERE enabled = TRUE;
|
||||||
|
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_schedule_next_run
|
||||||
|
ON compaction_schedule(next_run_at)
|
||||||
|
WHERE enabled = TRUE;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ROLLBACK INSTRUCTIONS
|
||||||
|
-- ============================================
|
||||||
|
-- DROP TABLE IF EXISTS compaction_schedule;
|
||||||
|
-- DROP TABLE IF EXISTS compaction_dryrun_result;
|
||||||
|
-- DROP TABLE IF EXISTS compaction_audit;
|
||||||
|
-- DROP TABLE IF EXISTS semantic_dedup_record;
|
||||||
|
-- DROP TABLE IF EXISTS stale_gc_record;
|
||||||
|
-- DROP TABLE IF EXISTS exact_dedup_record;
|
||||||
@@ -6,8 +6,10 @@
|
|||||||
use sqlx::{Pool, Postgres, Row, Transaction, Error as SqlxError};
|
use sqlx::{Pool, Postgres, Row, Transaction, Error as SqlxError};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use crate::entity_repo::{Entity, EntityRepo};
|
use mem_core::entity::Entity;
|
||||||
use crate::edge_repo::{Edge, EdgeRepo};
|
use mem_core::edge::Edge;
|
||||||
|
use crate::entity_repo::EntityRepoOps;
|
||||||
|
use crate::edge_repo::EdgeRepoOps;
|
||||||
|
|
||||||
/// Database connection error types
|
/// Database connection error types
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod edge_repo;
|
|||||||
pub mod community_repo;
|
pub mod community_repo;
|
||||||
pub mod versioning;
|
pub mod versioning;
|
||||||
pub mod audit_logger;
|
pub mod audit_logger;
|
||||||
|
// pub mod db_repo; // TODO: Fix Entity schema integration
|
||||||
|
|
||||||
pub use event_log::{EventRecord, LogWriter};
|
pub use event_log::{EventRecord, LogWriter};
|
||||||
pub use pgvector::{VectorRecord, VectorStore, ChunkL0, MemoryL1, MemoryL2};
|
pub use pgvector::{VectorRecord, VectorStore, ChunkL0, MemoryL1, MemoryL2};
|
||||||
|
|||||||
@@ -111,28 +111,28 @@ impl EntityVersioningService {
|
|||||||
let mut modified = Vec::new();
|
let mut modified = Vec::new();
|
||||||
|
|
||||||
// Check removed and modified
|
// Check removed and modified
|
||||||
if let Some(from) = from_obj {
|
if let Some(ref from) = from_obj {
|
||||||
for (key, from_val) in from {
|
for (key, from_val) in from {
|
||||||
if let Some(to) = &to_obj {
|
if let Some(to) = &to_obj {
|
||||||
if let Some(to_val) = to.get(&key) {
|
if let Some(to_val) = to.get(key) {
|
||||||
if from_val != *to_val {
|
if from_val != to_val {
|
||||||
modified.push(DiffField {
|
modified.push(DiffField {
|
||||||
name: key,
|
name: key.clone(),
|
||||||
from_value: Some(from_val),
|
from_value: Some(from_val.clone()),
|
||||||
to_value: Some(to_val.clone()),
|
to_value: Some(to_val.clone()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
removed.push(DiffField {
|
removed.push(DiffField {
|
||||||
name: key,
|
name: key.clone(),
|
||||||
from_value: Some(from_val),
|
from_value: Some(from_val.clone()),
|
||||||
to_value: None,
|
to_value: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
removed.push(DiffField {
|
removed.push(DiffField {
|
||||||
name: key,
|
name: key.clone(),
|
||||||
from_value: Some(from_val),
|
from_value: Some(from_val.clone()),
|
||||||
to_value: None,
|
to_value: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -301,28 +301,28 @@ fn compute_diff(
|
|||||||
let mut removed = Vec::new();
|
let mut removed = Vec::new();
|
||||||
let mut modified = Vec::new();
|
let mut modified = Vec::new();
|
||||||
|
|
||||||
if let Some(from) = from_obj {
|
if let Some(ref from) = from_obj {
|
||||||
for (key, from_val) in from {
|
for (key, from_val) in from {
|
||||||
if let Some(to) = &to_obj {
|
if let Some(to) = &to_obj {
|
||||||
if let Some(to_val) = to.get(&key) {
|
if let Some(to_val) = to.get(key) {
|
||||||
if from_val != *to_val {
|
if from_val != to_val {
|
||||||
modified.push(DiffField {
|
modified.push(DiffField {
|
||||||
name: key,
|
name: key.clone(),
|
||||||
from_value: Some(from_val),
|
from_value: Some(from_val.clone()),
|
||||||
to_value: Some(to_val.clone()),
|
to_value: Some(to_val.clone()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
removed.push(DiffField {
|
removed.push(DiffField {
|
||||||
name: key,
|
name: key.clone(),
|
||||||
from_value: Some(from_val),
|
from_value: Some(from_val.clone()),
|
||||||
to_value: None,
|
to_value: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
removed.push(DiffField {
|
removed.push(DiffField {
|
||||||
name: key,
|
name: key.clone(),
|
||||||
from_value: Some(from_val),
|
from_value: Some(from_val.clone()),
|
||||||
to_value: None,
|
to_value: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: ENC[AES256_GCM,data:TZY=,iv:sKXTsB2VO5ZAL6s/xzqcIgwtlNqk0xxOclb6ErYz02Q=,tag:yfK6Ax8mHaYZQVImcrsLcA==,type:str]
|
||||||
|
kind: ENC[AES256_GCM,data:Xr4rR7y/,iv:edAtbejU0PElYRZyAntlyuj5WnGf+Eksr+c636ovMc8=,tag:Hwv1higF27NzOTk/8EawYw==,type:str]
|
||||||
|
metadata:
|
||||||
|
name: ENC[AES256_GCM,data:Oe7W0UZxilfIUCIJRM4xP2sR,iv:w1NOlc20m8NX/WRT4JhXZ1MWjY0sFQa8ZJDeXemrL2M=,tag:cJ5TV8nKYHQhtLEnQa2mwQ==,type:str]
|
||||||
|
namespace: ENC[AES256_GCM,data:+4jnzJeY,iv:nfooi5BOr9Exx8r5m00HJRSnFYiSofV9/+heWjJNmww=,tag:TYoi+OJK6+1oe8SpyGeEEg==,type:str]
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: ENC[AES256_GCM,data:gxlL3o3C3mA9Th9OaQ==,iv:Hw4a9d9xAnX9MOdFkj3jf524I6wgBwPfoVYmXKUQgtU=,tag:9lTd7W1kx/Uv9E0MVMnclA==,type:str]
|
||||||
|
app.kubernetes.io/component: ENC[AES256_GCM,data:4DePKA==,iv:6f1Pvu+vWI+VkF8YrlSAM7yMvSckHWk+lu3qeMAz/ew=,tag:krszM09nNTo0EMtLJGw6Ew==,type:str]
|
||||||
|
type: ENC[AES256_GCM,data:Ri5WKSXq,iv:D1APSbaBiSL1FQS3eKQ2Jbl+BZ2EgKjIbpjh4TjRntU=,tag:rdCSkAFBkGn5qCmEZh6KxQ==,type:str]
|
||||||
|
stringData:
|
||||||
|
AUTHENTIK_ISSUER: ENC[AES256_GCM,data:LpLo+01McEw9gNwRqq06GgNXxi40QjmGXWKQfPBEw3rJ2vBaNyAbQv82vsFMdnrwUyTEulPsVvWHO9IS6fGip224UA==,iv:EUME2lcjH103kRgfktAnABT+HA5ckJ8FG/N8x/qKv5M=,tag:GhVZYiEFw8tSRUz/hxTROQ==,type:str]
|
||||||
|
AUTHENTIK_AUDIENCE: ENC[AES256_GCM,data:vhI7n2TG,iv:Q2E3aE6dx6BMqGHBNrEpUFwdEecmP/ZetkOmx5b4RB8=,tag:y+Wj9riWxFbOV+C04CGoJQ==,type:str]
|
||||||
|
JWT_CACHE_TTL_SECS: ENC[AES256_GCM,data:8ckJPw==,iv:/OL2ke9KSeUZHC2+wtqh7oNoU0YYlkxtbsXhjiCxLsk=,tag:LzPFyzFCXoaLs892/OKHCQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlSDFVZWdWZ0tVREV1UWVI
|
||||||
|
YVJ5eVVZdGcwSzdjREkzbkFLTmpWZi9mRnc0CkJ2Ujh4ZWVLMjFUczIzVmYwWEo4
|
||||||
|
eGpwZStvY1BYU1FHWEZDV1FhTE4zdWsKLS0tIDRJYmI5UzFVVFRCcCtUM1hYc3d3
|
||||||
|
S29NczhTb044SzlVWVBlN2paL0hzSEEKo/Q48c7IxhII1QJIwNDgUp7fSnbe2KOa
|
||||||
|
cnRlDnfN/6+6xTePnu+4uhmCJqHKprdpH/2RPrj51k4psgiOZdBi7w==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||||
|
lastmodified: "2026-09-08T05:17:56Z"
|
||||||
|
mac: ENC[AES256_GCM,data:ug9mPSxKAr2ZAci0S2DeYRrmaeQJLeC9N4B6fVRjy+pSaul2tfa9v6trTgrCnEI+VCqQMcDtlUQmrBy6CHoxlcHn8inHXMj+56mVAmxv0riHiyQOwtiZGVm1xX5kKA/z2q9tyT13aSPcsoVeuX6BOuwNjHCdAegsSEbZhuzrP4c=,iv:b2XdpqSeZAwNmbtSMwysHlsfPUJJCAN8qScWEj/JF9c=,tag:d2R/sQCFzB7+P2GUJGnHCg==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Non-sensitive environment variables for poimen-memory
|
||||||
|
# Change these without redeploying secrets.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: poimen-memory-config
|
||||||
|
namespace: poimen
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: poimen-memory
|
||||||
|
app.kubernetes.io/component: config
|
||||||
|
data:
|
||||||
|
# Auth mode: jwt | apikey
|
||||||
|
MEM_AUTH_MODE: "none"
|
||||||
|
# Rate limiting
|
||||||
|
MEM_RATE_LIMIT_INGEST: "100"
|
||||||
|
MEM_RATE_LIMIT_QUERY: "1000"
|
||||||
|
MEM_IDEMPOTENCY_TTL_SECS: "86400"
|
||||||
|
# Embeddings
|
||||||
|
MEM_EMBEDDING_BATCH_SIZE: "32"
|
||||||
|
# OpenSearch
|
||||||
|
OPENSEARCH_HOST: "opensearch.poimen.svc.cluster.local:9200"
|
||||||
|
# Obsidian
|
||||||
|
OBSIDIAN_URL: "http://obsidian-server.poimen.svc.cluster.local:8080"
|
||||||
@@ -29,7 +29,7 @@ spec:
|
|||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
containers:
|
containers:
|
||||||
- name: memory
|
- name: memory
|
||||||
image: forgejo.riotpiao.com/rock/poimen-memory:latest
|
image: forgejo.riotpiao.com/riotpiao-poimen/poimen-memory:latest
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
readOnlyRootFilesystem: true
|
readOnlyRootFilesystem: true
|
||||||
@@ -66,11 +66,16 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: poimen-memory-secrets
|
name: poimen-memory-secrets
|
||||||
key: llm-api-key
|
key: llm-api-key
|
||||||
# Server config
|
# Server config (from ConfigMap)
|
||||||
- name: MEM_PORT
|
- name: MEM_PORT
|
||||||
value: "8080"
|
value: "8080"
|
||||||
- name: MEM_HOME
|
- name: MEM_HOME
|
||||||
value: "/tmp"
|
value: "/tmp"
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: poimen-memory-config
|
||||||
|
- secretRef:
|
||||||
|
name: poimen-memory-auth
|
||||||
args:
|
args:
|
||||||
- serve
|
- serve
|
||||||
- --port
|
- --port
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ resources:
|
|||||||
# vault-pvc.yaml removed — memory service uses pgvector, not local storage
|
# vault-pvc.yaml removed — memory service uses pgvector, not local storage
|
||||||
- deployment.yaml
|
- deployment.yaml
|
||||||
- service.yaml
|
- service.yaml
|
||||||
|
- config.yaml
|
||||||
- obsidian.yaml
|
- obsidian.yaml
|
||||||
# Secret managed separately (SealedSecret in homelab)
|
# Legacy secret managed separately
|
||||||
# - secrets.yaml
|
# - secrets.yaml
|
||||||
|
generators:
|
||||||
|
- secret-generator.yaml
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ data:
|
|||||||
subject_key: "sub"
|
subject_key: "sub"
|
||||||
|
|
||||||
# JWKS endpoint from Authentik
|
# JWKS endpoint from Authentik
|
||||||
jwks_uri: "https://authentik.riotpiao.com/application/o/poimen-memory/jwks/"
|
jwks_uri: "https://authentik.riotpiao.com/application/o/poimen/jwks/"
|
||||||
jwks_refresh_interval_ms: 3600000 # 1 hour
|
jwks_refresh_interval_ms: 3600000 # 1 hour
|
||||||
|
|
||||||
# Issuer validation
|
# Issuer validation
|
||||||
issuer: "https://authentik.riotpiao.com/application/o/poimen-memory/"
|
issuer: "https://authentik.riotpiao.com/application/o/poimen/"
|
||||||
audience: null
|
audience: null
|
||||||
|
|
||||||
# Claims mapping
|
# Claims mapping
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# KSOPS generator — ArgoCD decrypts auth.enc.yaml at sync time
|
||||||
|
apiVersion: viaduct.ai/v1
|
||||||
|
kind: ksops
|
||||||
|
metadata:
|
||||||
|
name: poimen-memory-auth-generator
|
||||||
|
annotations:
|
||||||
|
config.kubernetes.io/function: |
|
||||||
|
exec:
|
||||||
|
path: ksops
|
||||||
|
files:
|
||||||
|
- auth.enc.yaml
|
||||||
Reference in New Issue
Block a user