Compare commits

..
Author SHA1 Message Date
rock fe3b84df0d ci: add deploy workflow — retag SHA as latest on main merge
CI / CI (pull_request) Successful in 11m13s
deploy.yaml (main push only):
  - Pull image by SHA tag (already pushed during PR CI)
  - Tag as :latest and push
  - No rebuild needed
2026-09-08 19:23:35 -07:00
rock 050b3625dc 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:23:08 -07:00
4 changed files with 53 additions and 57 deletions
+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_store::{MemoryL1, VectorStore, ChunkL0, EntityRepoOps, EdgeRepoOps};
use mem_llm::EmbeddingsClient; use mem_llm::EmbeddingsClient;
use mem_ingest::ingest_pipeline::{IngestPipeline, Episode}; 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::fact_extractor::SimpleFactExtractor;
use mem_ingest::contradiction_detector::ContradictionHandler; use mem_ingest::contradiction_detector::ContradictionHandler;
use sqlx::PgPool; use sqlx::PgPool;
@@ -26,16 +26,9 @@ impl IngestWorker {
) -> Self { ) -> Self {
let vector_store = Arc::new(VectorStore::new(pool.clone())); 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> = let entity_extractor: Arc<dyn mem_ingest::entity_extractor::EntityExtractor> =
if std::env::var("LLM_ENDPOINT").is_ok() { Arc::new(WikiLinkFallbackExtractor);
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)
};
let fact_extractor: Arc<dyn mem_ingest::fact_extractor::FactExtractor> = let fact_extractor: Arc<dyn mem_ingest::fact_extractor::FactExtractor> =
Arc::new(SimpleFactExtractor); Arc::new(SimpleFactExtractor);
let contradiction_detector = Arc::new(ContradictionHandler::default()); let contradiction_detector = Arc::new(ContradictionHandler::default());
+6 -46
View File
@@ -22,15 +22,11 @@ use tokio::sync::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedEntity { pub struct ExtractedEntity {
pub name: String, pub name: String,
#[serde(alias = "type")]
pub entity_type: EntityType, pub entity_type: EntityType,
pub summary: String, pub summary: String,
#[serde(default = "default_confidence")]
pub confidence: f32, pub confidence: f32,
} }
fn default_confidence() -> f32 { 0.8 }
impl ExtractedEntity { impl ExtractedEntity {
/// Convert to domain model (Phase 1 type) /// Convert to domain model (Phase 1 type)
pub fn to_domain(&self, project_id: &str) -> Entity { pub fn to_domain(&self, project_id: &str) -> Entity {
@@ -66,25 +62,6 @@ impl LlmEntityExtractor {
/// Parse extraction response JSON /// Parse extraction response JSON
/// Format: { "entities": [{ "name": "...", "type": "...", "summary": "..." }, ...] } /// 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>> { fn parse_extraction(response: &str) -> Result<Vec<ExtractedEntity>> {
#[derive(Deserialize)] #[derive(Deserialize)]
struct Response { struct Response {
@@ -169,16 +146,12 @@ impl LlmEntityExtractor {
} }
let data: serde_json::Value = response.json().await?; 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() .as_str()
.unwrap_or("{}") .unwrap_or("{}")
.to_string(); .to_string();
// Strip <think>...</think> tags from reasoning models tracing::debug!("LLM response (via Authentik JWT): {}", content);
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);
Ok(content) Ok(content)
} }
@@ -260,27 +233,14 @@ Respond in JSON:
); );
let reflection = if std::env::var("LLM_ENDPOINT").is_ok() { let reflection = if std::env::var("LLM_ENDPOINT").is_ok() {
self.call_llm_endpoint(&reflection_prompt).await.unwrap_or_else(|e| { self.call_llm_endpoint(&reflection_prompt).await.unwrap_or_else(|_| self.simulate_llm(&reflection_prompt).unwrap_or_default())
tracing::warn!("Reflection LLM call failed: {}, skipping verification", e);
String::new()
})
} else { } else {
self.simulate_llm(&reflection_prompt)? self.simulate_llm(&reflection_prompt)?
}; };
let verified = Self::parse_reflection(&reflection)?;
// If reflection succeeded, filter entities; otherwise keep all // Filter: keep only entities marked present
if !reflection.is_empty() { entities.retain(|e| verified.iter().any(|(name, present)| name == &e.name && *present));
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());
}
// Adjust confidence for reflected entities (slight penalty for needing verification) // Adjust confidence for reflected entities (slight penalty for needing verification)
for entity in &mut entities { for entity in &mut entities {
-1
View File
@@ -78,7 +78,6 @@ spec:
name: poimen-memory-auth name: poimen-memory-auth
- secretRef: - secretRef:
name: poimen-memory-secrets name: poimen-memory-secrets
command: ["/app/mem"]
args: args:
- serve - serve
- --port - --port