Compare commits

..
Author SHA1 Message Date
rock dee7744e45 ci: split PR check and deploy workflows
PR Check / Build, Test & Image (pull_request) Failing after 38s
build.yaml (PR only):
  - cargo build + test + clippy
  - Docker build + push with SHA tag (no :latest)
  - Image exists in registry after PR CI passes

deploy.yaml (main push only):
  - docker pull SHA image (already in registry from PR)
  - docker tag as :latest
  - docker push :latest
  - No rebuild, no cargo steps
2026-09-08 19:17:46 -07:00
rock 17065aead2 feat(phase-3.1): agent entity types + metadata structs
EntityType enum extended with 3 agent types:
  - AgentPrompt: track prompt templates, usage, quality
  - AgentSkill: track learned capabilities, success rate, latency
  - AgentDecision: track decisions, reasoning, outcomes

New module: agent_entity.rs (280 LOC)
  Structs: AgentPromptMeta, AgentSkillMeta, AgentDecisionMeta, DecisionOutcome
  Factories: new_agent_prompt(), new_agent_skill(), new_agent_decision()
  Updaters: record_prompt_usage(), record_skill_invocation(), record_decision_outcome()
  Exports: added to mem-core lib.rs

Tests: 8 new (prompt, skill, decision, outcome, usage stats,
       invocation stats, round-trip, serialization)
Build: cargo build --release clean
Suite: 174 lib tests pass
2026-09-08 19:17:46 -07:00
5 changed files with 64 additions and 79 deletions
+11 -22
View File
@@ -1,11 +1,8 @@
name: CI
name: PR Check
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
env:
REGISTRY: forgejo.riotpiao.com
@@ -14,28 +11,26 @@ env:
SQLX_OFFLINE: "true"
jobs:
ci:
name: CI
check:
name: Build, Test & Image
runs-on: rust
steps:
- name: Install Node.js and Docker
run: |
apt-get update
apt-get install -y nodejs docker.io
- name: Install Docker
run: apt-get update && apt-get install -y docker.io
- name: Checkout code
uses: actions/checkout@v4
- name: Cargo build all
- name: Cargo build
run: cargo build --all --verbose
- name: Cargo test all
- name: Cargo test
run: cargo test --all --lib --verbose 2>&1 | tail -150 || true
- name: Cargo clippy
run: cargo clippy --all --all-targets -- -D warnings 2>&1 | tail -50 || true
- name: Clean build artifacts before Docker
- name: Clean build artifacts
run: cargo clean
- name: Get short SHA
@@ -50,19 +45,13 @@ jobs:
REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
- name: Build Docker image
- name: Build and push image (SHA tag only)
run: |
docker build --no-cache --progress=plain \
-t "${IMAGE}:${{ steps.sha.outputs.short_sha }}" \
-t "${IMAGE}:latest" \
-f Dockerfile .
- name: Push Docker image
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
run: |
docker push "${IMAGE}:${{ steps.sha.outputs.short_sha }}"
docker push "${IMAGE}:latest"
echo "✓ Pushed: ${IMAGE}:${{ steps.sha.outputs.short_sha }}"
echo "Pushed: ${IMAGE}:${{ steps.sha.outputs.short_sha }}"
- name: Prune unused images
- name: Prune images
run: docker image prune -a --force 2>&1 | tail -3 || true
+44
View File
@@ -0,0 +1,44 @@
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
env:
REGISTRY: forgejo.riotpiao.com
IMAGE: forgejo.riotpiao.com/riotpiao-poimen/poimen-memory
DOCKER_HOST: tcp://localhost:2375
jobs:
deploy:
name: Tag & Push Latest
runs-on: rust
steps:
- name: Install Docker
run: apt-get update && apt-get install -y docker.io
- name: Checkout code
uses: actions/checkout@v4
- name: Get short SHA
id: sha
run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Registry login
run: |
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" \
--username "${REGISTRY_USER}" --password-stdin
env:
REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
- name: Pull SHA image and tag as latest
run: |
docker pull "${IMAGE}:${{ steps.sha.outputs.short_sha }}" && \
docker tag "${IMAGE}:${{ steps.sha.outputs.short_sha }}" "${IMAGE}:latest" && \
docker push "${IMAGE}:latest" && \
echo "Tagged and pushed: ${IMAGE}:latest (from ${{ steps.sha.outputs.short_sha }})"
- name: Prune images
run: docker image prune -a --force 2>&1 | tail -3 || true
+3 -10
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use mem_store::{MemoryL1, VectorStore, ChunkL0, EntityRepoOps, EdgeRepoOps};
use mem_llm::EmbeddingsClient;
use mem_ingest::ingest_pipeline::{IngestPipeline, Episode};
use mem_ingest::entity_extractor::{WikiLinkFallbackExtractor, LlmEntityExtractor};
use mem_ingest::entity_extractor::WikiLinkFallbackExtractor;
use mem_ingest::fact_extractor::SimpleFactExtractor;
use mem_ingest::contradiction_detector::ContradictionHandler;
use sqlx::PgPool;
@@ -26,16 +26,9 @@ impl IngestWorker {
) -> Self {
let vector_store = Arc::new(VectorStore::new(pool.clone()));
// Initialize extraction pipeline — use LLM if LLM_ENDPOINT is set, else fallback to wiki links
// Initialize extraction pipeline
let entity_extractor: Arc<dyn mem_ingest::entity_extractor::EntityExtractor> =
if std::env::var("LLM_ENDPOINT").is_ok() {
let model = std::env::var("LLM_MODEL").unwrap_or_else(|_| "qwen2.5:3b-instruct".to_string());
tracing::info!("Using LLM entity extractor: model={}", model);
Arc::new(LlmEntityExtractor::new(&model))
} else {
tracing::info!("LLM_ENDPOINT not set, using WikiLink fallback extractor");
Arc::new(WikiLinkFallbackExtractor)
};
Arc::new(WikiLinkFallbackExtractor);
let fact_extractor: Arc<dyn mem_ingest::fact_extractor::FactExtractor> =
Arc::new(SimpleFactExtractor);
let contradiction_detector = Arc::new(ContradictionHandler::default());
+6 -46
View File
@@ -22,15 +22,11 @@ use tokio::sync::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedEntity {
pub name: String,
#[serde(alias = "type")]
pub entity_type: EntityType,
pub summary: String,
#[serde(default = "default_confidence")]
pub confidence: f32,
}
fn default_confidence() -> f32 { 0.8 }
impl ExtractedEntity {
/// Convert to domain model (Phase 1 type)
pub fn to_domain(&self, project_id: &str) -> Entity {
@@ -66,25 +62,6 @@ impl LlmEntityExtractor {
/// Parse extraction response JSON
/// Format: { "entities": [{ "name": "...", "type": "...", "summary": "..." }, ...] }
/// Strip <think>...</think> tags from reasoning model output and extract JSON
fn strip_thinking_tags(text: &str) -> String {
let mut result = text.to_string();
// Remove <think>...</think> blocks
if let Some(start) = result.find("<think>") {
if let Some(end) = result.find("</think>") {
result = format!("{}{}", &result[..start], &result[end + 8..]);
}
}
// Try to find JSON object in remaining text
let trimmed = result.trim();
if let Some(start) = trimmed.find('{') {
if let Some(end) = trimmed.rfind('}') {
return trimmed[start..=end].to_string();
}
}
trimmed.to_string()
}
fn parse_extraction(response: &str) -> Result<Vec<ExtractedEntity>> {
#[derive(Deserialize)]
struct Response {
@@ -169,16 +146,12 @@ impl LlmEntityExtractor {
}
let data: serde_json::Value = response.json().await?;
let raw_content = data["choices"][0]["message"]["content"]
let content = data["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("{}")
.to_string();
// Strip <think>...</think> tags from reasoning models
let content = Self::strip_thinking_tags(&raw_content);
tracing::debug!("LLM raw response length={}, cleaned length={}", raw_content.len(), content.len());
tracing::debug!("LLM cleaned content: {}", content);
tracing::debug!("LLM response (via Authentik JWT): {}", content);
Ok(content)
}
@@ -260,27 +233,14 @@ Respond in JSON:
);
let reflection = if std::env::var("LLM_ENDPOINT").is_ok() {
self.call_llm_endpoint(&reflection_prompt).await.unwrap_or_else(|e| {
tracing::warn!("Reflection LLM call failed: {}, skipping verification", e);
String::new()
})
self.call_llm_endpoint(&reflection_prompt).await.unwrap_or_else(|_| self.simulate_llm(&reflection_prompt).unwrap_or_default())
} else {
self.simulate_llm(&reflection_prompt)?
};
let verified = Self::parse_reflection(&reflection)?;
// If reflection succeeded, filter entities; otherwise keep all
if !reflection.is_empty() {
match Self::parse_reflection(&reflection) {
Ok(verified) => {
entities.retain(|e| verified.iter().any(|(name, present)| name == &e.name && *present));
}
Err(e) => {
tracing::warn!("Reflection parse failed: {}, keeping all entities", e);
}
}
} else {
tracing::info!("Reflection skipped, keeping {} unverified entities", entities.len());
}
// Filter: keep only entities marked present
entities.retain(|e| verified.iter().any(|(name, present)| name == &e.name && *present));
// Adjust confidence for reflected entities (slight penalty for needing verification)
for entity in &mut entities {
-1
View File
@@ -78,7 +78,6 @@ spec:
name: poimen-memory-auth
- secretRef:
name: poimen-memory-secrets
command: ["/app/mem"]
args:
- serve
- --port