Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "knowforge",
"private": true,
"version": "0.7.1",
"version": "0.7.2",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "knowforge"
version = "0.7.1"
version = "0.7.2"
description = "Knowforge desktop app"
authors = ["caichangqing"]
edition = "2024"
Expand Down
8 changes: 6 additions & 2 deletions src-tauri/src/challenge_review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use std::fs;
use std::path::{Path, PathBuf};
use uuid::Uuid;

use std::sync::Arc;

use crate::llm::{create_provider, CompletionOverrides};
use crate::llm::LlmChatMessage;
use crate::thought_parser;
Expand Down Expand Up @@ -272,6 +274,7 @@ fn normalize_template_kind(raw: Option<&str>) -> String {
#[tauri::command]
pub async fn generate_challenge_question(
workspace: tauri::State<'_, crate::WorkspaceState>,
http_client: tauri::State<'_, Arc<reqwest::Client>>,
args: GenerateChallengeQuestionArgs,
) -> Result<GenerateChallengeQuestionResponse, String> {
let root = crate::lock_workspace_root(&workspace)?;
Expand All @@ -282,7 +285,7 @@ pub async fn generate_challenge_question(
.await
.map_err(|e| e.to_string())??;

let provider = match create_provider(&ai, None) {
let provider = match create_provider(&ai, None, http_client.inner()) {
Ok(p) => p,
Err(_) => {
return Ok(GenerateChallengeQuestionResponse {
Expand Down Expand Up @@ -407,6 +410,7 @@ pub async fn generate_challenge_question(
#[tauri::command]
pub async fn evaluate_challenge_answer(
workspace: tauri::State<'_, crate::WorkspaceState>,
http_client: tauri::State<'_, Arc<reqwest::Client>>,
args: EvaluateChallengeAnswerArgs,
) -> Result<EvaluateChallengeAnswerResponse, String> {
let root = crate::lock_workspace_root(&workspace)?;
Expand All @@ -424,7 +428,7 @@ pub async fn evaluate_challenge_answer(
}
};

let provider = match create_provider(&ai, None) {
let provider = match create_provider(&ai, None, http_client.inner()) {
Ok(p) => p,
Err(_) => {
return Ok(EvaluateChallengeAnswerResponse {
Expand Down
12 changes: 10 additions & 2 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1540,11 +1540,13 @@ async fn semantic_search(
args: semantic_index::SemanticSearchArgs,
app: tauri::AppHandle,
state: tauri::State<'_, WorkspaceState>,
embed_cache: tauri::State<'_, Arc<semantic_index::EmbeddingCache>>,
) -> Result<Vec<semantic_index::SemanticSearchHit>, String> {
let root = lock_workspace_root(&state)?;
let cache = semantic_index::default_model_cache_dir();
let bundle = semantic_index::resolve_bundle_model_dir(&app);
tauri::async_runtime::spawn_blocking(move || semantic_index::run_semantic_search(&root, &cache, &bundle, args))
let embed_cache_arc = Arc::clone(embed_cache.inner());
tauri::async_runtime::spawn_blocking(move || semantic_index::run_semantic_search(&root, &cache, &bundle, args, &embed_cache_arc))
.await
.map_err(|e| e.to_string())?
}
Expand All @@ -1557,6 +1559,8 @@ async fn suggest_related_notes(
include_reasons: Option<bool>,
editor_markdown_override: Option<String>,
state: tauri::State<'_, WorkspaceState>,
http_client: tauri::State<'_, Arc<reqwest::Client>>,
embed_cache: tauri::State<'_, Arc<semantic_index::EmbeddingCache>>,
) -> Result<Vec<link_recommendation::LinkRecommendation>, String> {
let canonical_root = lock_workspace_root(&state)?;
let rel_path = rel_path.trim().to_string();
Expand All @@ -1569,6 +1573,7 @@ async fn suggest_related_notes(
let root = canonical_root.clone();
let rel = rel_path.clone();
let md_override = editor_markdown_override.clone();
let embed_cache_arc = Arc::clone(embed_cache.inner());
let mut out = tauri::async_runtime::spawn_blocking(move || -> Result<Vec<link_recommendation::LinkRecommendation>, String> {
let emb = semantic_index::open_embedding_db(&root)?;
let thoughts = vault_thoughts_db::open_thoughts_db(&root)?;
Expand All @@ -1579,6 +1584,7 @@ async fn suggest_related_notes(
&thoughts,
max_results,
md_override.as_deref(),
&embed_cache_arc,
)
})
.await
Expand All @@ -1593,7 +1599,7 @@ async fn suggest_related_notes(
})
.await
.map_err(|e| e.to_string())??;
link_recommendation::enrich_recommendations_with_reasons(&mut out, &excerpt, &ai).await?;
link_recommendation::enrich_recommendations_with_reasons(&mut out, &excerpt, &ai, http_client.inner()).await?;
}

Ok(out)
Expand Down Expand Up @@ -1655,6 +1661,8 @@ async fn add_manual_topic_semantic(
pub fn run() {
tauri::Builder::default()
.manage(WorkspaceState::default())
.manage(Arc::new(llm::build_shared_http_client()))
.manage(Arc::new(semantic_index::EmbeddingCache::new()))
.manage(Arc::new(llm::LlmSessionState::default()))
.manage(Arc::new(llm::approval::ToolApprovalState::new()))
.manage(Arc::new(tools::ToolRegistry::new()))
Expand Down
22 changes: 16 additions & 6 deletions src-tauri/src/link_recommendation.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! 文档级语义相似度 → 双向链接推荐候选(迭代 6.3 步骤 15)。
//! 无语义索引时不提供推荐(无关键词兜底)。

use std::sync::Arc;

use crate::llm::create_provider;
use crate::llm::LlmChatMessage;
use crate::note_privacy;
Expand Down Expand Up @@ -435,6 +437,7 @@ pub fn suggest_related_notes(
_thoughts_db: &Connection,
max_results: usize,
editor_markdown_override: Option<&str>,
embed_cache: &semantic_index::EmbeddingCache,
) -> Result<Vec<LinkRecommendation>, String> {
if max_results == 0 {
return Ok(Vec::new());
Expand All @@ -456,7 +459,7 @@ pub fn suggest_related_notes(
return Err("semantic_index_not_ready: kf-private notes are excluded from link recommendations".to_string());
}

let all_chunks = semantic_index::load_all_doc_embeddings(embedding_db)?;
let all_chunks = embed_cache.get_docs(embedding_db);
if all_chunks.is_empty() {
return Err(
"semantic_index_not_ready: no document chunks in embedding index; rebuild embeddings first"
Expand Down Expand Up @@ -623,8 +626,9 @@ async fn link_reason_completion_body(
ai: &ResolvedAiConfig,
messages: &[LlmChatMessage],
timeout_ms: u64,
http_client: &Arc<reqwest::Client>,
) -> Option<String> {
let provider = create_provider(ai, None).ok()?;
let provider = create_provider(ai, None, http_client).ok()?;
let overrides = crate::llm::CompletionOverrides {
timeout_ms: Some(timeout_ms),
..Default::default()
Expand All @@ -639,6 +643,7 @@ pub async fn enrich_recommendations_with_reasons(
candidates: &mut [LinkRecommendation],
current_doc_excerpt: &str,
config: &ResolvedAiConfig,
http_client: &Arc<reqwest::Client>,
) -> Result<(), String> {
if candidates.is_empty() {
return Ok(());
Expand Down Expand Up @@ -680,7 +685,7 @@ pub async fn enrich_recommendations_with_reasons(
},
];

let raw = match link_reason_completion_body(config, &msgs, timeout_ms).await {
let raw = match link_reason_completion_body(config, &msgs, timeout_ms, http_client).await {
Some(s) => {
let t = s.trim();
if t.is_empty() {
Expand Down Expand Up @@ -813,7 +818,8 @@ mod tests {
semantic_index::upsert_doc_chunk(&conn, "far.md#0", "far.md", 0, "t", &far, "m").unwrap();

let tconn = Connection::open_in_memory().unwrap();
let rec = suggest_related_notes(root, "cur.md", &conn, &tconn, 5, None).unwrap();
let ec = semantic_index::EmbeddingCache::new();
let rec = suggest_related_notes(root, "cur.md", &conn, &tconn, 5, None, &ec).unwrap();
assert_eq!(rec.len(), 2);
assert_eq!(rec[0].target_rel_path, "near.md");
assert_eq!(rec[1].target_rel_path, "mid.md");
Expand All @@ -837,7 +843,8 @@ mod tests {
semantic_index::upsert_doc_chunk(&conn, "linked#0", "linked", 0, "t", &near, "m").unwrap();

let tconn = Connection::open_in_memory().unwrap();
let rec = suggest_related_notes(root, "cur.md", &conn, &tconn, 5, None).expect("chunks align");
let ec = semantic_index::EmbeddingCache::new();
let rec = suggest_related_notes(root, "cur.md", &conn, &tconn, 5, None, &ec).expect("chunks align");
assert_eq!(rec.len(), 1);
assert_eq!(rec[0].target_rel_path, "near.md");
}
Expand All @@ -862,13 +869,15 @@ mod tests {
semantic_index::upsert_doc_chunk(&conn, "far.md#0", "far.md", 0, "t", &far, "m").unwrap();

let tconn = Connection::open_in_memory().unwrap();
let ec = semantic_index::EmbeddingCache::new();
let rec = suggest_related_notes(
root,
"cur.md",
&conn,
&tconn,
5,
Some("x [[linked]]\n"),
&ec,
)
.unwrap();
assert_eq!(rec.len(), 1);
Expand Down Expand Up @@ -931,7 +940,8 @@ mod tests {
existing_link: false,
reason: None,
}];
super::enrich_recommendations_with_reasons(&mut c, "excerpt", &ai)
let http_client = Arc::new(reqwest::Client::new());
super::enrich_recommendations_with_reasons(&mut c, "excerpt", &ai, &http_client)
.await
.unwrap();
assert!(c[0].reason.is_none());
Expand Down
22 changes: 19 additions & 3 deletions src-tauri/src/llm/agent_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ use std::time::Duration;

use futures_util::future::join_all;
use serde_json::{json, Value};
use tauri::{AppHandle, Emitter};
use tauri::{AppHandle, Emitter, Manager};
use tokio_util::sync::CancellationToken;

use super::approval::ToolApprovalState;
use super::context_guard::ContextGuard;
use super::context_guard::{ContextGuard, PrecomputedSummary};
use super::memory;
use super::provider::{LlmProvider, NormalizedToolCall};
use super::{LlmChatMessage, LlmToolCall, LlmToolCallFunction};
Expand Down Expand Up @@ -137,6 +137,7 @@ pub async fn run_agent_stream(
ContextGuard::with_provider(config.max_context_tokens, provider.clone())
};
let mut loop_detector = LoopDetector::new();
let mut pending_summary: Option<tokio::task::JoinHandle<Option<PrecomputedSummary>>> = None;

loop {
if cancel.is_cancelled() {
Expand All @@ -154,6 +155,11 @@ pub async fn run_agent_stream(
}
}

if let Some(handle) = pending_summary.take() {
if let Ok(Some(cached)) = handle.await {
context_guard.apply_cached_summary(&mut messages, &cached);
}
}
context_guard.trim_with_summary(&mut messages).await;

// 1. 流式请求(携带 tools 字段;本轮文字会通过 emit_chunk/emit_done 推给前端)
Expand Down Expand Up @@ -384,7 +390,14 @@ pub async fn run_agent_stream(
store_extraction_msgs(&memory_manager, &messages).await;
return String::new();
}
// 继续 loop:下一轮流式请求会带上完整的 messages 历史

if context_guard.budget_pressure(&messages) > 0.7 {
let msgs_snapshot = messages.clone();
let guard_clone = context_guard.clone();
pending_summary = Some(tokio::spawn(async move {
guard_clone.pre_summarize(&msgs_snapshot).await
}));
}
}
}

Expand Down Expand Up @@ -484,6 +497,9 @@ pub(crate) async fn execute_tool(
);
ctx.call_id = Some(tc.id.clone());
ctx.provider = provider;
if let Some(ec) = app.try_state::<Arc<crate::semantic_index::EmbeddingCache>>() {
ctx.embed_cache = Some(Arc::clone(&*ec));
}

let manifest = tool.manifest().clone();
let start = std::time::Instant::now();
Expand Down
Loading