diff --git a/README.md b/README.md index 9b66345..9add875 100644 --- a/README.md +++ b/README.md @@ -317,7 +317,15 @@ openclaw plugins install @memclaw/memclaw ## Documentation -For detailed configuration, troubleshooting, and best practices, see the [MemClaw README](examples/@memclaw/plugin/README.md). +For current maintenance status and repository details, see the [MemClaw project notice](examples/@memclaw/README.md). + +--- + +# Source Memory Example: TweetClaw X/Twitter Signals + +Cortex Memory can keep reviewed public X/Twitter research from TweetClaw as durable source memory without storing credentials, raw exports, or private account data. This is useful for OpenClaw agents that collect market, support, competitor, or community signals and need to recall why a decision was made later. + +See [TweetClaw X/Twitter source memory](examples/tweetclaw-x-signal-memory.md) for setup, storage shape, and search examples. --- diff --git a/cortex-mem-cli/src/commands/search.rs b/cortex-mem-cli/src/commands/search.rs index a843361..e28d9f4 100644 --- a/cortex-mem-cli/src/commands/search.rs +++ b/cortex-mem-cli/src/commands/search.rs @@ -48,6 +48,7 @@ pub async fn execute( threshold: min_score, root_uri: Some(scope_uri.clone()), recursive: true, + precomputed_intent: None, }; // Perform layered vector search (L0/L1/L2 hierarchical search) diff --git a/cortex-mem-core/src/search/vector_engine.rs b/cortex-mem-core/src/search/vector_engine.rs index 272e783..73155c0 100644 --- a/cortex-mem-core/src/search/vector_engine.rs +++ b/cortex-mem-core/src/search/vector_engine.rs @@ -17,7 +17,7 @@ use tokio::sync::mpsc; use tracing::{debug, info, warn}; /// Search options -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct SearchOptions { /// Maximum number of results pub limit: usize, @@ -27,6 +27,10 @@ pub struct SearchOptions { pub root_uri: Option, /// Enable recursive search pub recursive: bool, + /// Precomputed intent from LLM analysis. + /// If provided, semantic_search skips intent analysis and reuses this intent. + /// This reduces LLM calls from 5 per search → 1 per search. + pub precomputed_intent: Option>, } impl Default for SearchOptions { @@ -36,6 +40,7 @@ impl Default for SearchOptions { threshold: 0.6, root_uri: None, recursive: true, + precomputed_intent: None, } } } @@ -262,13 +267,30 @@ impl VectorSearchEngine { Some((scope, owner_id, memory_id)) } + /// Parse root_uri to extract (scope, owner_id) for filtering. + /// e.g. "cortex://session/wecom-alis" -> Some(("session", "wecom-alis")) + fn parse_root_uri(root_uri: &str) -> Option<(String, String)> { + let stripped = root_uri.strip_prefix("cortex://")?; + let parts: Vec<&str> = stripped.splitn(3, '/').collect(); + if parts.len() < 2 { + return None; + } + Some((parts[0].to_string(), parts[1].to_string())) + } + /// Semantic search using vector similarity pub async fn semantic_search( &self, query: &str, options: &SearchOptions, ) -> Result> { - let intent = self.analyze_intent(query).await?; + // Reuse precomputed intent if available (reduces LLM calls from 5 → 1 per search) + let intent = if let Some(ref precomputed) = options.precomputed_intent { + info!("semantic_search: reusing precomputed intent (type={:?})", precomputed.intent_type); + (**precomputed).clone() + } else { + self.analyze_intent(query).await? + }; let query_text = if intent.rewritten_query.trim().is_empty() { query } else { @@ -279,7 +301,13 @@ impl VectorSearchEngine { let mut filters = crate::types::Filters::default(); if let Some(scope) = &options.root_uri { - filters.uri_prefix = Some(scope.clone()); + // Set owner_scope + uri_prefix so qdrant-level filtering uses exact scope + if let Some((owner_scope, _owner_id)) = Self::parse_root_uri(scope) { + filters.owner_scope = Some(owner_scope); + filters.uri_prefix = Some(scope.clone()); + } else { + filters.uri_prefix = Some(scope.clone()); + } } let scored = self @@ -355,8 +383,13 @@ impl VectorSearchEngine { query: &str, options: &SearchOptions, ) -> Result> { - // 1. LLM 统一意图分析(单次请求) - let intent = self.analyze_intent(query).await?; + // Reuse precomputed intent if available (reduces LLM calls from 5 → 1 per search) + let intent = if let Some(ref precomputed) = options.precomputed_intent { + info!("layered_semantic_search: reusing precomputed intent (type={:?})", precomputed.intent_type); + (**precomputed).clone() + } else { + self.analyze_intent(query).await? + }; info!( "Intent analysis: type={:?}, entities={:?}, keywords={:?}, rewritten='{}'", @@ -376,6 +409,9 @@ impl VectorSearchEngine { ); let mut l0_filters = crate::types::Filters::with_layer("L0"); if let Some(scope) = &options.root_uri { + if let Some((owner_scope, _owner_id)) = Self::parse_root_uri(scope) { + l0_filters.owner_scope = Some(owner_scope); + } l0_filters.uri_prefix = Some(scope.clone()); } @@ -560,7 +596,7 @@ impl VectorSearchEngine { } /// 统一意图分析(优先使用 LLM 单次调用,LLM 不可用时使用最小 fallback) - async fn analyze_intent(&self, query: &str) -> Result { + pub async fn analyze_intent(&self, query: &str) -> Result { if self.enable_intent_analysis { if let Some(llm) = &self.llm_client { match self.analyze_intent_with_llm(llm.as_ref(), query).await { diff --git a/cortex-mem-core/src/types.rs b/cortex-mem-core/src/types.rs index 81255fe..30b54ac 100644 --- a/cortex-mem-core/src/types.rs +++ b/cortex-mem-core/src/types.rs @@ -221,6 +221,8 @@ pub struct Filters { pub max_importance: Option, /// URI prefix filter for scope-based searching pub uri_prefix: Option, + /// Owner scope hint: "session", "agent", or "user" (used with uri_prefix to construct qdrant filter) + pub owner_scope: Option, pub custom: HashMap, } diff --git a/cortex-mem-core/src/vector_store/qdrant.rs b/cortex-mem-core/src/vector_store/qdrant.rs index 6527dd2..ac42487 100644 --- a/cortex-mem-core/src/vector_store/qdrant.rs +++ b/cortex-mem-core/src/vector_store/qdrant.rs @@ -343,6 +343,21 @@ impl QdrantVectorStore { fn filters_to_qdrant_filter(&self, filters: &Filters) -> Option { let mut conditions = Vec::new(); + // Filter by scope + uri_prefix as a Match on the uri field + // This enables session/agent/user scope filtering at the qdrant level + if filters.owner_scope.is_some() && filters.uri_prefix.is_some() { + let uri_prefix = filters.uri_prefix.as_ref().unwrap(); + conditions.push(Condition { + condition_one_of: Some(condition::ConditionOneOf::Field(FieldCondition { + key: "uri".to_string(), + r#match: Some(Match { + match_value: Some(r#match::MatchValue::Text(uri_prefix.clone())), + }), + ..Default::default() + })), + }); + } + if let Some(user_id) = &filters.user_id { conditions.push(Condition { condition_one_of: Some(condition::ConditionOneOf::Field(FieldCondition { diff --git a/cortex-mem-service/Dockerfile b/cortex-mem-service/Dockerfile new file mode 100644 index 0000000..d98b638 --- /dev/null +++ b/cortex-mem-service/Dockerfile @@ -0,0 +1,12 @@ +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY target/release/cortex-mem-service /cortex-mem-service + +ENV CORTEX_DATA_DIR=/mnt/sata-trace/cortex-mem/data +ENV CORTEX_TENANT_ID=tenant_claw + +ENTRYPOINT ["/cortex-mem-service"] diff --git a/cortex-mem-service/src/handlers/filesystem.rs b/cortex-mem-service/src/handlers/filesystem.rs index 7289ea4..79f553c 100644 --- a/cortex-mem-service/src/handlers/filesystem.rs +++ b/cortex-mem-service/src/handlers/filesystem.rs @@ -519,6 +519,7 @@ pub async fn explore( threshold: 0.3, // Lower threshold for exploration root_uri: Some(req.start_uri.clone()), recursive: true, + precomputed_intent: None, }; let search_results = vector_engine diff --git a/cortex-mem-service/src/handlers/search.rs b/cortex-mem-service/src/handlers/search.rs index 0ae4925..f8cc57e 100644 --- a/cortex-mem-service/src/handlers/search.rs +++ b/cortex-mem-service/src/handlers/search.rs @@ -59,6 +59,7 @@ async fn search_layered( threshold: min_score, root_uri: None, recursive: true, + precomputed_intent: None, }; let mut semantic_options = options.clone(); semantic_options.threshold = (min_score * 0.5).max(0.0); @@ -85,6 +86,14 @@ async fn search_layered( let profile = build_query_profile(query); + // [优化] 一次性 intent 分析,后续 semantic_search 复用(5次LLM→1次) + let precomputed_intent = vector_engine.analyze_intent(query).await?; + let precomputed_intent = Arc::new(precomputed_intent); + options.precomputed_intent = Some(precomputed_intent.clone()); + semantic_options.precomputed_intent = Some(precomputed_intent.clone()); + tracing::info!("[search优化] intent precomputed: type={:?}, keywords={:?}", + precomputed_intent.intent_type, precomputed_intent.keywords); + let layered_results = vector_engine .layered_semantic_search(query, &options) .await diff --git a/cortex-mem-service/src/main.rs b/cortex-mem-service/src/main.rs index a209aa6..9947be9 100644 --- a/cortex-mem-service/src/main.rs +++ b/cortex-mem-service/src/main.rs @@ -1,7 +1,7 @@ use axum::{Router, routing::get}; use clap::Parser; use std::fs::File; -use std::net::SocketAddr; +use std::net::{SocketAddr, IpAddr}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use tower_http::cors::CorsLayer; @@ -139,7 +139,8 @@ async fn main() -> anyhow::Result<()> { .with_state(state); // Start server - let addr = SocketAddr::from(([127, 0, 0, 1], cli.port)); + let ip: IpAddr = cli.host.parse().unwrap_or(IpAddr::from([0, 0, 0, 0])); + let addr = SocketAddr::from((ip, cli.port)); info!("Server listening on http://{}", addr); let listener = tokio::net::TcpListener::bind(addr).await?; diff --git a/cortex-mem-tools/src/tools/search.rs b/cortex-mem-tools/src/tools/search.rs index f640a59..20b0c5f 100644 --- a/cortex-mem-tools/src/tools/search.rs +++ b/cortex-mem-tools/src/tools/search.rs @@ -117,6 +117,7 @@ impl MemoryOperations { threshold: 0.5, root_uri: args.scope.clone(), recursive: args.recursive.unwrap_or(true), + precomputed_intent: None, }; // Use layered semantic search for L0/L1/L2 tiered retrieval diff --git a/examples/tweetclaw-x-signal-memory.md b/examples/tweetclaw-x-signal-memory.md new file mode 100644 index 0000000..72645fb --- /dev/null +++ b/examples/tweetclaw-x-signal-memory.md @@ -0,0 +1,72 @@ +# TweetClaw X/Twitter Source Memory + +Use this recipe when an OpenClaw agent gathers public X/Twitter context with TweetClaw and stores only reviewed source notes in Cortex Memory. + +## Install TweetClaw + +```bash +openclaw plugins install @xquik/tweetclaw +openclaw plugins inspect tweetclaw --runtime +openclaw config set tools.alsoAllow '["explore", "tweetclaw"]' +openclaw config set plugins.entries.tweetclaw.config.apiKey "$XQUIK_API_KEY" +``` + +Keep the key in OpenClaw plugin config or environment-backed secret storage. Do not paste API keys, cookies, DMs, private account data, raw exports, or media files into Cortex memories. + +## Collect And Review Sources + +Start with `explore` to inspect the TweetClaw endpoint shape, then call `tweetclaw` only for the fields the agent needs. Typical source workflows include: + +- Search tweets for a product, competitor, keyword, or campaign. +- Search tweet replies to understand objections, support themes, or follow-up questions. +- Look up public user context when the workflow needs a handle, display name, profile URL, or public metrics. +- Export follower context only when the account owner and workflow allow it. +- Read monitor or webhook events only for monitors the user configured. + +Review the results before writing memory. Store conclusions and source references, not raw timelines. + +## Store Reviewed Notes + +Use one session thread per research run so Cortex Memory can later extract a compact L0/L1/L2 memory trail. + +```bash +cortex-mem --config config.toml --tenant acme add \ + --thread tweetclaw-research-2026-05-24 \ + --role user \ + "TweetClaw public X/Twitter search for query 'openclaw memory plugin' found recurring requests for examples that connect social signals to durable agent memory. Sources reviewed: tweet IDs 1234567890 and 2345678901. Decision: add a source-memory recipe and avoid storing raw exports." + +cortex-mem --config config.toml --tenant acme session close tweetclaw-research-2026-05-24 +``` + +A reviewed note should usually include: + +- Query, endpoint, and capture date. +- Tweet IDs, URLs, author handles, or monitor event IDs. +- The decision, evidence summary, and next action. +- Confidence and any known gaps. + +Use `cortex://resources/x-twitter/tweetclaw/...` paths for reusable source summaries and session memory for one-off investigations. + +## Search Later + +```bash +cortex-mem --config config.toml --tenant acme search \ + "why did we add a TweetClaw source memory example?" \ + --thread tweetclaw-research-2026-05-24 \ + --scope session \ + --limit 5 +``` + +If the answer should outlive the run, promote the final summary into a shared resource note after review. + +## Approval Boundaries + +TweetClaw can support write workflows such as post tweets, post tweet replies, media upload, and direct messages. Do not store or replay pending write actions from memory without a fresh explicit user approval. Treat Cortex Memory as source context and decision history, not an action queue. + +## Verification Checklist + +- TweetClaw installs with `openclaw plugins install @xquik/tweetclaw`. +- `openclaw plugins inspect tweetclaw --runtime` reports the plugin runtime as loaded. +- Cortex Memory can add, close, and search the research session. +- Stored notes contain reviewed summaries and source references only. +- No API keys, cookies, DMs, raw exports, or private account material are stored.