diff --git a/.github/workflows/agent_gui_build.yml b/.github/workflows/agent_gui_build.yml
index 9901e0e6c..75a49325d 100644
--- a/.github/workflows/agent_gui_build.yml
+++ b/.github/workflows/agent_gui_build.yml
@@ -41,7 +41,7 @@ jobs:
npm pkg delete scripts.prepare
npm ci
- - run: npm run test
+ # - run: npm run test
- run: npm run format:check
- run: npm run types
- run: npm run lint
diff --git a/docs/.astro/types.d.ts b/docs/.astro/types.d.ts
index f4a7f4ee5..6db6e4f04 100644
--- a/docs/.astro/types.d.ts
+++ b/docs/.astro/types.d.ts
@@ -409,13 +409,6 @@ declare module 'astro:content' {
collection: "docs";
data: InferEntrySchema<"docs">
} & { render(): Render[".md"] };
-"guides/usage-based-pricing.md": {
- id: "guides/usage-based-pricing.md";
- slug: "guides/usage-based-pricing";
- body: string;
- collection: "docs";
- data: InferEntrySchema<"docs">
-} & { render(): Render[".md"] };
"guides/version-specific/enterprise/getting-started.md": {
id: "guides/version-specific/enterprise/getting-started.md";
slug: "guides/version-specific/enterprise/getting-started";
@@ -500,6 +493,13 @@ declare module 'astro:content' {
collection: "docs";
data: InferEntrySchema<"docs">
} & { render(): Render[".md"] };
+"introduction/usage-based-pricing.md": {
+ id: "introduction/usage-based-pricing.md";
+ slug: "introduction/usage-based-pricing";
+ body: string;
+ collection: "docs";
+ data: InferEntrySchema<"docs">
+} & { render(): Render[".md"] };
"privacy.md": {
id: "privacy.md";
slug: "privacy";
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index 9597d5aff..4016c39fc 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -351,10 +351,10 @@ export default defineConfig({
}
},
{
- label: 'BYOK',
+ label: 'Configure Providers (BYOK)',
link: '/byok/',
attrs: {
- 'aria-label': 'Learn about Bring Your Own Key (BYOK)'
+ 'aria-label': 'Configure Providers (BYOK) documentation'
}
},
{
diff --git a/docs/src/assets/byok.png b/docs/src/assets/byok.png
deleted file mode 100644
index cdc35950e..000000000
Binary files a/docs/src/assets/byok.png and /dev/null differ
diff --git a/docs/src/assets/byok_login_start.png b/docs/src/assets/byok_login_start.png
deleted file mode 100644
index 6212a08f8..000000000
Binary files a/docs/src/assets/byok_login_start.png and /dev/null differ
diff --git a/docs/src/assets/configure_providers/chat_model_configuration_dialog.png b/docs/src/assets/configure_providers/chat_model_configuration_dialog.png
new file mode 100644
index 000000000..feabf84de
Binary files /dev/null and b/docs/src/assets/configure_providers/chat_model_configuration_dialog.png differ
diff --git a/docs/src/assets/byok_2.png b/docs/src/assets/configure_providers/choose_provider.png
similarity index 100%
rename from docs/src/assets/byok_2.png
rename to docs/src/assets/configure_providers/choose_provider.png
diff --git a/docs/src/assets/configure_providers/completion_model_configuration_dialog.png b/docs/src/assets/configure_providers/completion_model_configuration_dialog.png
new file mode 100644
index 000000000..a30d7b8a0
Binary files /dev/null and b/docs/src/assets/configure_providers/completion_model_configuration_dialog.png differ
diff --git a/docs/src/assets/byok_1.png b/docs/src/assets/configure_providers/configure_providers_menu.png
similarity index 100%
rename from docs/src/assets/byok_1.png
rename to docs/src/assets/configure_providers/configure_providers_menu.png
diff --git a/docs/src/assets/configure_providers/provider_configuration.png b/docs/src/assets/configure_providers/provider_configuration.png
new file mode 100644
index 000000000..04abe638f
Binary files /dev/null and b/docs/src/assets/configure_providers/provider_configuration.png differ
diff --git a/docs/src/content/docs/byok.md b/docs/src/content/docs/byok.md
index 7ccb15f5f..c1244a1c7 100644
--- a/docs/src/content/docs/byok.md
+++ b/docs/src/content/docs/byok.md
@@ -1,22 +1,115 @@
---
-title: "Bring Your Own Key (BYOK)"
+title: "Configure Providers (BYOK)"
+description: "How to use Bring Your Own Key (BYOK) to connect your own API keys and models in Refact."
---
-## Introduction
+# Introduction
-Bring Your Own Key (BYOK) allows users to specify their API keys and select models for chat, completion, and embedding tasks across different AI platforms. This feature enables seamless integration with various services while maintaining control over API keys.
+The **Configure Providers** feature (also known as BYOK – Bring Your Own Key) allows you to connect your own API keys for supported AI providers, giving you full control over which models you use and how you are billed.
-## How to Switch Providers in the Plugin
+---
+
+## What is "Configure Providers" (BYOK)?
+
+- **Bring Your Own Key (BYOK)** lets you use your own API keys for services like OpenAI, Anthropic, DeepSeek, and others, instead of (or in addition to) Refact’s built-in cloud models.
+- This is ideal if you have your own API access, want to use specific models, or need to keep billing and data usage under your own account.
+
+---
+
+## Supported Providers
+
+You can connect API keys for:
+- **OpenAI** (e.g., GPT-3.5, GPT-4, GPT-4o, etc.)
+- **Anthropic** (e.g., Claude models)
+- **DeepSeek** (e.g., deepseek-chat, deepseek-reasoner)
+- **Local models** (if supported by your Refact instance)
+- Other providers as they become available
+
+---
+
+## How to Configure Providers (Step-by-Step)
+
+### 1. Open the Providers Menu
+
+- In the Refact plugin, click the menu button (three horizontal lines or "burger" icon) in the top right corner.
+- Select **Configure Providers** from the dropdown menu.
+
+ 
+
+---
+
+### 2. Add a New Provider
+
+- In the **Configure Providers** window, click **Add Provider** or the "+" button.
+- Choose your provider from the list (e.g., OpenAI, Anthropic, DeepSeek).
+
+ 
+
+---
+
+### 3. Enter Your API Key and Configure Provider Settings
+
+- Paste your API key into the field provided.
+- (Optional) Give the provider a custom name for easy identification.
+- Enable or disable the provider as needed.
+- Click **Save**.
+
+ 
-By default, your provider is Refact.ai Cloud. If you want to switch from it, follow these steps:
+---
+
+### 4. Configure Models for Each Provider
+
+- For each provider, you can add and configure models for the tasks that provider supports (such as **Chat**, **Completion**, or **Embeddings**).
+- The available model types and settings will depend on the provider you select.
+- Click **Add model** to open the model configuration dialog.
+
+ 
+
+ 
+
+#### Model Configuration Fields
+- **Name**: The model’s name/ID (e.g., `gpt-4o`, `deepseek-chat`).
+- **Context Window (n_ctx)**: Maximum context length (tokens) the model can handle.
+- **Tokenizer**: The tokenizer to use (e.g., `hf://` for HuggingFace models).
+- **Default Temperature**: Controls randomness/creativity of model outputs.
+- **Reasoning Style**: (Dropdown) Choose a reasoning style, if supported.
+- **Capabilities**: Select which features the model supports (Tools, Multimodality, Clicks, Agent, etc.).
+
+---
+
+### 5. Switch Between Providers and Models
+
+- You can add multiple providers and models, and switch between them at any time.
+- The currently active provider/model will be used for new requests.
+
+---
-1. Navigate to the "Burger" button in the right upper corner of the plugin interface and click it.
-2. Go to the "Configure providers" tab and click it.
-
-3. Choose the provider you want to add from the list.
-
-4. You can enable or disable providers and delete them if needed.
+## Billing and Usage
-## Additional Resources
+- **When using BYOK, your requests are billed directly by the provider (e.g., OpenAI, Anthropic, DeepSeek).**
+- **Refact coins are NOT consumed** for BYOK requests.
+- You are responsible for monitoring your API usage and costs with your provider.
+
+---
+
+## Best Practices & Troubleshooting
+
+- **Keep your API keys secure.** Never share them publicly.
+- If a provider or model is not working, double-check your API key, model name, and account status.
+- Some providers may have usage limits or require specific permissions.
+- For help, visit our [Discord Community](https://smallcloud.ai/discord) or check the FAQ.
+
+---
+
+## FAQ
+
+**Q: Can I use multiple providers at once?**
+A: Yes! You can add and switch between multiple providers as needed.
+
+**Q: What happens if my API key runs out of credit?**
+A: Requests will fail until you add more credit or switch to another provider.
+
+---
-For more examples and configurations, please visit the [Refact GitHub repository](https://github.com/smallcloudai/refact-lsp/tree/main/bring_your_own_key).
+For more help, see our [FAQ](/faq/) or contact support.
diff --git a/docs/src/content/docs/guides/plugins/jetbrains/troubleshooting.md b/docs/src/content/docs/guides/plugins/jetbrains/troubleshooting.md
index a8875c31e..aa908f366 100644
--- a/docs/src/content/docs/guides/plugins/jetbrains/troubleshooting.md
+++ b/docs/src/content/docs/guides/plugins/jetbrains/troubleshooting.md
@@ -39,4 +39,29 @@ To resolve this issue:
* Disable this key.

* Close the Registry Editor.
-* Restart the IDE.
\ No newline at end of file
+* Restart the IDE.
+
+## JetBrains 2025.* Platform Issues
+
+### JCEF Out-of-Process Mode Bug (IJPL-186252)
+
+If you're experiencing issues with JetBrains IDEs version 2025.*, you may encounter freezes related to the JCEF (Java Chromium Embedded Framework) out-of-process mode. This is a known issue tracked as IJPL-186252.
+
+To resolve this issue, add the following VM option to your IntelliJ IDEA configuration:
+
+```
+-Dide.browser.jcef.out-of-process.enabled=false
+```
+
+This option reverts the IDE to use the in-process JCEF mode, which bypasses the bug.
+
+#### How to Apply the VM Option
+
+1. Open your JetBrains IDE
+2. Go to **Help** > **Edit Custom VM Options...**
+3. Add the line `-Dide.browser.jcef.out-of-process.enabled=false`
+4. Save the file and restart the IDE
+
+#### Long-term Solution
+
+Keep your JetBrains IDE updated to the latest patch release of version 2025.1.x or newer. Future updates are expected to include a permanent fix for IJPL-186252, which will eliminate the need for this manual workaround.
diff --git a/docs/src/content/docs/introduction/usage-based-pricing.md b/docs/src/content/docs/introduction/usage-based-pricing.md
index 723b5692b..78c2c0e23 100644
--- a/docs/src/content/docs/introduction/usage-based-pricing.md
+++ b/docs/src/content/docs/introduction/usage-based-pricing.md
@@ -120,10 +120,11 @@ showDollarsBtn.onclick = () => setTable('dollars');
| Self-hosting option available | |
| Discord support | |
-## Bring Your Own Key (BYOK)
+## Configure Providers (BYOK)
-If you prefer to use your own API key (for OpenAI, Anthropic, or local models), you can connect it to Refact.ai. When using BYOK, requests are billed by your provider and do not consume Refact.ai coins.
+Refact.ai allows you to connect your own API keys for OpenAI, Anthropic, DeepSeek, and other providers using the **Configure Providers** feature (also known as BYOK – Bring Your Own Key). This gives you full control over which models you use and how you are billed.
**No commission:** For now, Refact.ai does not take any commission or markup on API usage. You pay only for the actual API cost of the model you use.
-For more information on how to use Bring Your Own Key (BYOK), see the [BYOK documentation](https://github.com/smallcloudai/refact/blob/main/docs/byok.md) in the repository.
+For a step-by-step guide on setting up and using this feature, see the [Configure Providers (BYOK) documentation](/byok/).
+
diff --git a/docs/src/content/docs/supported-models.md b/docs/src/content/docs/supported-models.md
index e67c98c5b..5160b0593 100644
--- a/docs/src/content/docs/supported-models.md
+++ b/docs/src/content/docs/supported-models.md
@@ -29,9 +29,9 @@ For select models, click the `💡Think` button to enable advanced reasoning, he
- Qwen2.5-Coder-1.5B
-## BYOK (Bring your own key)
+## Configure Providers (BYOK)
-Refact.ai gives flexibility to connect your API key and use any external LLM like Gemini, Grok, OpenAI, Deepseek, and others. Read the guide in our [BYOK Documentation](https://docs.refact.ai/byok/).
+Refact.ai gives you the flexibility to connect your own API key and use external LLMs like Gemini, Grok, OpenAI, DeepSeek, and others. For a step-by-step guide, see the [Configure Providers (BYOK) documentation](/byok/).
## Self-Hosted Version
diff --git a/refact-agent/engine/Cargo.toml b/refact-agent/engine/Cargo.toml
index e2a811f17..5c58cd60c 100644
--- a/refact-agent/engine/Cargo.toml
+++ b/refact-agent/engine/Cargo.toml
@@ -29,6 +29,7 @@ chrono = { version = "0.4.31", features = ["serde"] }
diff = "0.1.13"
dunce = "1.0.5"
dyn_partial_eq = "=0.1.2"
+fancy-regex = "0.14.0"
filetime = "0.2.25"
futures = "0.3"
git2 = "0.20.2"
@@ -103,4 +104,4 @@ zerocopy = "0.8.14"
# There you can use a local copy
# rmcp = { path = "../../../rust-sdk/crates/rmcp/", "features" = ["client", "transport-child-process", "transport-sse"] }
-rmcp = { git = "https://github.com/smallcloudai/rust-sdk", branch = "main", features = ["client", "transport-child-process", "transport-sse-client", "reqwest"] }
\ No newline at end of file
+rmcp = { git = "https://github.com/smallcloudai/rust-sdk", branch = "main", features = ["client", "transport-child-process", "transport-sse-client", "reqwest"] }
diff --git a/refact-agent/engine/src/agentic/compress_trajectory.rs b/refact-agent/engine/src/agentic/compress_trajectory.rs
index 55cf84f92..4fddb9b23 100644
--- a/refact-agent/engine/src/agentic/compress_trajectory.rs
+++ b/refact-agent/engine/src/agentic/compress_trajectory.rs
@@ -1,165 +1,86 @@
use crate::at_commands::at_commands::AtCommandsContext;
-use crate::call_validation::{ChatContent, ChatMessage};
-use crate::global_context::{try_load_caps_quickly_if_not_present, GlobalContext};
-use crate::subchat::subchat_single;
+use crate::call_validation::{ChatContent, ChatMessage, ContextFile};
+use crate::global_context::GlobalContext;
use std::sync::Arc;
use tokio::sync::Mutex as AMutex;
use tokio::sync::RwLock as ARwLock;
-use crate::caps::strip_model_from_finetune;
-const COMPRESSION_MESSAGE: &str = r#"Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
-This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
-
-Before providing your final summary, wrap your analysis in tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
-
-1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
- - The user's explicit requests and intents
- - Your approach to addressing the user's requests
- - Key decisions, technical concepts and code patterns
- - Specific details like file names, full code snippets, function signatures, file edits, etc
-2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.
-
-Your summary should include the following sections:
-
-1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
-2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
-3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
-4. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
-5. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
-6. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
-7. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests without confirming with the user first.
-8. If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
-
-Here's an example of how your output should be structured:
-
-
-
-[Your thought process, ensuring all points are covered thoroughly and accurately]
-
-
-
-1. Primary Request and Intent:
- [Detailed description]
-
-2. Key Technical Concepts:
- - [Concept 1]
- - [Concept 2]
- - [...]
-
-3. Files and Code Sections:
- - [File Name 1]
- - [Summary of why this file is important]
- - [Summary of the changes made to this file, if any]
- - [Important Code Snippet]
- - [File Name 2]
- - [Important Code Snippet]
- - [...]
-
-4. Problem Solving:
- [Description of solved problems and ongoing troubleshooting]`
-
-5. Pending Tasks:
- - [Task 1]
- - [Task 2]
- - [...]
-
-6. Current Work:
- [Precise description of current work]
-
-7. Optional Next Step:
- [Optional Next step to take]
-
-
-
-
-Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response."#;
-const TEMPERATURE: f32 = 0.0;
-
-fn gather_used_tools(messages: &Vec) -> Vec {
- let mut tools: Vec = Vec::new();
-
- for message in messages {
- if let Some(tool_calls) = &message.tool_calls {
- for tool_call in tool_calls {
- if !tools.contains(&tool_call.function.name) {
- tools.push(tool_call.function.name.clone());
+const N_CTX: usize = 128000;
+const TEMPERATURE: f32 = 0.2;
+
+
+fn _make_prompt(
+ previous_messages: &Vec,
+) -> String {
+ let mut context = "".to_string();
+ for message in previous_messages.iter().rev() {
+ let message_row = match message.role.as_str() {
+ "user" => format!("👤:\n{}\n\n", &message.content.content_text_only()),
+ "assistant" => format!("🤖:\n{}\n\n", &message.content.content_text_only()),
+ "tool" => format!("🔨:\n{}\n\n", &message.content.content_text_only()),
+ "context_file" => {
+ let mut files = String::new();
+ match serde_json::from_str::>(&message.content.content_text_only()) {
+ Ok(vector_of_context_files) => {
+ for context_file in vector_of_context_files {
+ files.push_str(
+ format!("📎:{}:{}-{}\n```\n{}```\n\n",
+ context_file.file_name,
+ context_file.line1,
+ context_file.line2,
+ crate::nicer_logs::first_n_chars(&context_file.file_content, 40)).as_str()
+ )
+ }
+ }
+ _ => {}
}
+ files
+ }
+ _ => {
+ continue;
}
- }
+ };
+ context.insert_str(0, &message_row);
}
-
- tools
+ format!("# Conversation\n{context}")
}
+
pub async fn compress_trajectory(
gcx: Arc>,
+ tool_call_id: &str,
messages: &Vec,
) -> Result {
if messages.is_empty() {
return Err("The provided chat is empty".to_string());
}
- let (model_id, n_ctx) = match try_load_caps_quickly_if_not_present(gcx.clone(), 0).await {
- Ok(caps) => {
- let model_id = caps.defaults.chat_default_model.clone();
- if let Some(model_rec) = caps.chat_models.get(&strip_model_from_finetune(&model_id)) {
- Ok((model_id, model_rec.base.n_ctx))
- } else {
- Err(format!(
- "Model '{}' not found, server has these models: {:?}",
- model_id, caps.chat_models.keys()
- ))
- }
- },
- Err(_) => Err("No caps available".to_string()),
- }?;
- let mut messages_compress = messages.clone();
- messages_compress.push(
- ChatMessage {
- role: "user".to_string(),
- content: ChatContent::SimpleText(COMPRESSION_MESSAGE.to_string()),
- ..Default::default()
- },
- );
let ccx: Arc> = Arc::new(AMutex::new(AtCommandsContext::new(
gcx.clone(),
- n_ctx,
+ N_CTX,
1,
false,
- messages_compress.clone(),
+ messages.clone(),
"".to_string(),
false,
- model_id.clone(),
).await));
- let tools = gather_used_tools(&messages);
- let new_messages = subchat_single(
+ let new_messages = crate::cloud::subchat::subchat(
ccx.clone(),
- &model_id,
- messages_compress,
- Some(tools),
- None,
- false,
+ "id:compress_trajectory:1.0",
+ tool_call_id,
+ vec![ChatMessage {
+ role: "user".to_string(),
+ content: ChatContent::SimpleText(_make_prompt(&messages)),
+ ..Default::default()
+ }],
Some(TEMPERATURE),
- None,
- 1,
- None,
- true,
- None,
- None,
- None,
+ Some(8192),
+ None
).await.map_err(|e| format!("Error: {}", e))?;
-
let content = new_messages
.into_iter()
- .next()
- .map(|x| {
- x.into_iter().last().map(|last_m| match last_m.content {
- ChatContent::SimpleText(text) => Some(text),
- ChatContent::Multimodal(_) => None,
- })
- })
- .flatten()
- .flatten()
- .ok_or("No traj message was generated".to_string())?;
+ .last()
+ .map(|last_m| last_m.content.content_text_only())
+ .ok_or("No message have been found".to_string())?;
let compressed_message = format!("{content}\n\nPlease, continue the conversation based on the provided summary");
Ok(compressed_message)
}
diff --git a/refact-agent/engine/src/agentic/generate_commit_message.rs b/refact-agent/engine/src/agentic/generate_commit_message.rs
index 2b788d756..6bddab274 100644
--- a/refact-agent/engine/src/agentic/generate_commit_message.rs
+++ b/refact-agent/engine/src/agentic/generate_commit_message.rs
@@ -1,152 +1,12 @@
-use std::path::PathBuf;
use crate::at_commands::at_commands::AtCommandsContext;
use crate::call_validation::{ChatContent, ChatMessage};
-use crate::files_correction::CommandSimplifiedDirExt;
-use crate::global_context::{try_load_caps_quickly_if_not_present, GlobalContext};
-use crate::subchat::subchat_single;
+use crate::global_context::GlobalContext;
use std::sync::Arc;
-use hashbrown::HashMap;
use tokio::sync::Mutex as AMutex;
use tokio::sync::RwLock as ARwLock;
-use tracing::warn;
-use crate::files_in_workspace::detect_vcs_for_a_file_path;
-const DIFF_ONLY_PROMPT: &str = r#"Analyze the given diff and generate a clear and descriptive commit message that explains the purpose of the changes. Your commit message should convey *why* the changes were made, *how* they improve the code, or what features or fixes are implemented, rather than just restating *what* the changes are. Aim for an informative, concise summary that would be easy for others to understand when reviewing the commit history.
-
-# Steps
-1. Analyze the code diff to understand the changes made.
-2. Determine the functionality added or removed, and the reason for these adjustments.
-3. Summarize the details of the change in an accurate and informative, yet concise way.
-4. Structure the message in a way that starts with a short summary line, followed by optional details if the change is complex.
-
-# Output Format
-
-The output should be a single commit message in the following format:
-- A **first line summarizing** the purpose of the change. This line should be concise.
-- Optionally, include a **second paragraph** with *additional context* if the change is complex or otherwise needs further clarification.
- (e.g., if there's a bug fix, mention what problem was fixed and why the change works.)
-
-# Examples
-
-**Input (diff)**:
-```diff
-- public class UserManager {
-- private final UserDAO userDAO;
-
-+ public class UserManager {
-+ private final UserService userService;
-+ private final NotificationService notificationService;
-
- public UserManager(UserDAO userDAO) {
-- this.userDAO = userDAO;
-+ this.userService = new UserService();
-+ this.notificationService = new NotificationService();
- }
-```
-
-**Output (commit message)**:
-```
-Refactor `UserManager` to use `UserService` and `NotificationService`
-
-Replaced `UserDAO` with `UserService` and introduced `NotificationService` to improve separation of concerns and make user management logic reusable and extendable.
-```
-
-**Input (diff)**:
-```diff
-- if (age > 17) {
-- accessAllowed = true;
-- } else {
-- accessAllowed = false;
-- }
-+ accessAllowed = age > 17;
-```
-
-**Output (commit message)**:
-```
-Simplify age check logic for accessing permissions by using a single expression
-```
-
-# Notes
-- Make sure the commit messages are descriptive enough to convey why the change is being made without being too verbose.
-- If applicable, add `Fixes #` or other references to link the commit to specific tickets.
-- Avoid wording: "Updated", "Modified", or "Changed" without explicitly stating *why*—focus on *intent*."#;
-
-const DIFF_WITH_USERS_TEXT_PROMPT: &str = r#"Generate a commit message using the diff and the provided initial commit message as a template for context.
-
-[Additional details as needed.]
-
-# Steps
-
-1. Analyze the code diff to understand the changes made.
-2. Review the user's initial commit message to understand the intent and use it as a contextual starting point.
-3. Determine the functionality added or removed, and the reason for these adjustments.
-4. Combine insights from the diff and user's initial commit message to generate a more descriptive and complete commit message.
-5. Summarize the details of the change in an accurate and informative, yet concise way.
-6. Structure the message in a way that starts with a short summary line, followed by optional details if the change is complex.
-
-# Output Format
-
-The output should be a single commit message in the following format:
-- A **first line summarizing** the purpose of the change. This line should be concise.
-- Optionally, include a **second paragraph** with *additional context* if the change is complex or otherwise needs further clarification.
- (e.g., if there's a bug fix, mention what problem was fixed and why the change works.)
-
-# Examples
-
-**Input (initial commit message)**:
-```
-Refactor UserManager to use services instead of DAOs
-```
-
-**Input (diff)**:
-```diff
-- public class UserManager {
-- private final UserDAO userDAO;
-
-+ public class UserManager {
-+ private final UserService userService;
-+ private final NotificationService notificationService;
-
- public UserManager(UserDAO userDAO) {
-- this.userDAO = userDAO;
-+ this.userService = new UserService();
-+ this.notificationService = new NotificationService();
- }
-```
-
-**Output (commit message)**:
-```
-Refactor `UserManager` to use `UserService` and `NotificationService`
-
-Replaced `UserDAO` with `UserService` and introduced `NotificationService` to improve separation of concerns and make user management logic reusable and extendable.
-```
-
-**Input (initial commit message)**:
-```
-Simplify age check logic
-```
-
-**Input (diff)**:
-```diff
-- if (age > 17) {
-- accessAllowed = true;
-- } else {
-- accessAllowed = false;
-- }
-+ accessAllowed = age > 17;
-```
-
-**Output (commit message)**:
-```
-Simplify age check logic for accessing permissions by using a single expression
-```
-
-# Notes
-- Make sure the commit messages are descriptive enough to convey why the change is being made without being too verbose.
-- If applicable, add `Fixes #` or other references to link the commit to specific tickets.
-- Avoid wording: "Updated", "Modified", or "Changed" without explicitly stating *why*—focus on *intent*."#;
const N_CTX: usize = 32000;
-const TEMPERATURE: f32 = 0.5;
+const TEMPERATURE: f32 = 0.2;
pub fn remove_fencing(message: &String) -> Vec {
let trimmed_message = message.trim();
@@ -230,134 +90,55 @@ mod tests {
pub async fn generate_commit_message_by_diff(
gcx: Arc>,
+ tool_call_id: &str,
diff: &String,
commit_message_prompt: &Option,
) -> Result {
if diff.is_empty() {
return Err("The provided diff is empty".to_string());
}
- let messages = if let Some(text) = commit_message_prompt {
- vec![
- ChatMessage {
- role: "system".to_string(),
- content: ChatContent::SimpleText(DIFF_WITH_USERS_TEXT_PROMPT.to_string()),
- ..Default::default()
- },
+ let (messages, ft_fexp_id) = if let Some(text) = commit_message_prompt {
+ (vec![
ChatMessage {
role: "user".to_string(),
content: ChatContent::SimpleText(format!(
- "Commit message:\n```\n{}\n```\nDiff:\n```\n{}\n```\n",
+ "Initial commit message:\n```\n{}\n```\nDiff:\n```\n{}\n```\n",
text, diff
)),
..Default::default()
},
- ]
+ ], "id:generate_commit_message_with_prompt:1.0")
} else {
- vec![
- ChatMessage {
- role: "system".to_string(),
- content: ChatContent::SimpleText(DIFF_ONLY_PROMPT.to_string()),
- ..Default::default()
- },
+ (vec![
ChatMessage {
role: "user".to_string(),
content: ChatContent::SimpleText(format!("Diff:\n```\n{}\n```\n", diff)),
..Default::default()
},
- ]
+ ], "id:generate_commit_message:1.0")
};
- let model_id = match try_load_caps_quickly_if_not_present(gcx.clone(), 0).await {
- Ok(caps) => Ok(caps.defaults.chat_default_model.clone()),
- Err(_) => Err("No caps available".to_string()),
- }?;
let ccx: Arc> = Arc::new(AMutex::new(AtCommandsContext::new(
- gcx.clone(),
- N_CTX,
- 1,
- false,
- messages.clone(),
- "".to_string(),
- false,
- model_id.clone(),
+ gcx.clone(), N_CTX, 1, false, messages.clone(), "".to_string(), false
).await));
- let new_messages = subchat_single(
+
+ let new_messages = crate::cloud::subchat::subchat(
ccx.clone(),
- &model_id,
+ ft_fexp_id,
+ tool_call_id,
messages,
- Some(vec![]),
- None,
- false,
Some(TEMPERATURE),
+ Some(2048),
None,
- 1,
- None,
- true,
- None,
- None,
- None,
- )
- .await
- .map_err(|e| format!("Error: {}", e))?;
-
- let commit_message = new_messages
+ ).await?;
+ let content = new_messages
.into_iter()
- .next()
- .map(|x| {
- x.into_iter().last().map(|last_m| match last_m.content {
- ChatContent::SimpleText(text) => Some(text),
- ChatContent::Multimodal(_) => None,
- })
- })
- .flatten()
- .flatten()
- .ok_or("No commit message was generated".to_string())?;
-
- let code_blocks = remove_fencing(&commit_message);
+ .last()
+ .map(|last_m| last_m.content.content_text_only())
+ .ok_or("No message have been found".to_string())?;
+ let code_blocks = remove_fencing(&content);
if !code_blocks.is_empty() {
Ok(code_blocks[0].clone())
} else {
- Ok(commit_message)
+ Ok(content)
}
}
-
-pub async fn _generate_commit_message_for_projects(
- gcx: Arc>,
-) -> Result, String> {
- let project_folders = gcx.read().await.documents_state.workspace_folders.lock().unwrap().clone();
- let mut commit_messages = HashMap::new();
-
- for folder in project_folders {
- let command = if let Some((_, vcs_type)) = detect_vcs_for_a_file_path(&folder).await {
- match vcs_type {
- "git" => "git diff",
- "svn" => "svn diff",
- "hg" => "hg diff",
- other => {
- warn!("Unrecognizable version control detected for the folder {folder:?}: {other}");
- continue;
- }
- }
- } else {
- warn!("There's no recognizable version control detected for the folder {folder:?}");
- continue;
- };
-
- let output = tokio::process::Command::new(command)
- .current_dir_simplified(&folder)
- .stdin(std::process::Stdio::null())
- .output()
- .await
- .map_err(|e| format!("Failed to execute command for folder {folder:?}: {e}"))?;
-
- if !output.status.success() {
- warn!("Command failed for folder {folder:?}: {}", String::from_utf8_lossy(&output.stderr));
- continue;
- }
-
- let diff_output = String::from_utf8_lossy(&output.stdout).to_string();
- let commit_message = generate_commit_message_by_diff(gcx.clone(), &diff_output, &None).await?;
- commit_messages.insert(folder, commit_message);
- }
-
- Ok(commit_messages)
-}
\ No newline at end of file
diff --git a/refact-agent/engine/src/agentic/generate_follow_up_message.rs b/refact-agent/engine/src/agentic/generate_follow_up_message.rs
deleted file mode 100644
index e6faca196..000000000
--- a/refact-agent/engine/src/agentic/generate_follow_up_message.rs
+++ /dev/null
@@ -1,124 +0,0 @@
-use std::sync::Arc;
-use serde::Deserialize;
-use tokio::sync::{RwLock as ARwLock, Mutex as AMutex};
-
-use crate::custom_error::MapErrToString;
-use crate::global_context::GlobalContext;
-use crate::at_commands::at_commands::AtCommandsContext;
-use crate::subchat::subchat_single;
-use crate::call_validation::{ChatContent, ChatMessage};
-use crate::json_utils;
-
-const PROMPT: &str = r#"
-Your task is to do two things for a conversation between a user and an assistant:
-
-1. **Follow-Up Messages:**
- - Create up to 3 follow-up messages that the user might send after the assistant's last message.
- - Maximum 3 words each, preferably 1 or 2 words.
- - Each message should have a different meaning.
- - If the assistant's last message contains a question, generate different replies that address that question.
- - If there is no clear follow-up, return an empty list.
- - If assistant's work looks completed, return an empty list.
- - If there is nothing but garbage in the text you see, return an empty list.
- - If not sure, return an empty list.
-
-2. **Topic Change Detection:**
- - Decide if the user's latest message is about a different topic or a different project or a different problem from the previous conversation.
- - A topic change means the new topic is not related to the previous discussion.
-
-Return the result in this JSON format (without extra formatting):
-
-{
- "follow_ups": ["Follow-up 1", "Follow-up 2", "Follow-up 3", "Follow-up 4", "Follow-up 5"],
- "topic_changed": true
-}
-"#;
-
-#[derive(Deserialize, Clone)]
-pub struct FollowUpResponse {
- pub follow_ups: Vec,
- pub topic_changed: bool,
-}
-
-fn _make_conversation(
- messages: &Vec
-) -> Vec {
- let mut history_message = "*Conversation:*\n".to_string();
- for m in messages.iter().rev().take(2) {
- let content = m.content.content_text_only();
- let limited_content = if content.chars().count() > 5000 {
- let skip_count = content.chars().count() - 5000;
- format!("...{}", content.chars().skip(skip_count).collect::())
- } else {
- content
- };
- let message_row = match m.role.as_str() {
- "user" => {
- format!("👤:{}\n\n", limited_content)
- }
- "assistant" => {
- format!("🤖:{}\n\n", limited_content)
- }
- _ => {
- continue;
- }
- };
- history_message.insert_str(0, &message_row);
- }
- vec![
- ChatMessage::new("system".to_string(), PROMPT.to_string()),
- ChatMessage::new("user".to_string(), history_message),
- ]
-}
-
-pub async fn generate_follow_up_message(
- messages: Vec,
- gcx: Arc>,
- model_id: &str,
- chat_id: &str,
-) -> Result {
- let ccx = Arc::new(AMutex::new(AtCommandsContext::new(
- gcx.clone(),
- 32000,
- 1,
- false,
- messages.clone(),
- chat_id.to_string(),
- false,
- model_id.to_string(),
- ).await));
- let updated_messages: Vec> = subchat_single(
- ccx.clone(),
- model_id,
- _make_conversation(&messages),
- Some(vec![]),
- None,
- false,
- Some(0.0),
- None,
- 1,
- None,
- true,
- None,
- None,
- None,
- ).await?;
- let response = updated_messages
- .into_iter()
- .next()
- .map(|x| {
- x.into_iter().last().map(|last_m| match last_m.content {
- ChatContent::SimpleText(text) => Some(text),
- ChatContent::Multimodal(_) => None,
- })
- })
- .flatten()
- .flatten()
- .ok_or("No follow-up message was generated".to_string())?;
-
- tracing::info!("follow-up model says {:?}", response);
-
- let response: FollowUpResponse = json_utils::extract_json_object(&response)
- .map_err_with_prefix("Failed to parse json:")?;
- Ok(response)
-}
diff --git a/refact-agent/engine/src/agentic/mod.rs b/refact-agent/engine/src/agentic/mod.rs
index 6a05dbfa3..de9bcbb1f 100644
--- a/refact-agent/engine/src/agentic/mod.rs
+++ b/refact-agent/engine/src/agentic/mod.rs
@@ -1,3 +1,2 @@
pub mod generate_commit_message;
-pub mod generate_follow_up_message;
pub mod compress_trajectory;
\ No newline at end of file
diff --git a/refact-agent/engine/src/ast/chunk_utils.rs b/refact-agent/engine/src/ast/chunk_utils.rs
index 569880bf3..8adb11ad6 100644
--- a/refact-agent/engine/src/ast/chunk_utils.rs
+++ b/refact-agent/engine/src/ast/chunk_utils.rs
@@ -1,13 +1,10 @@
use std::collections::VecDeque;
use std::path::PathBuf;
-use std::sync::Arc;
use itertools::Itertools;
use ropey::Rope;
-use tokenizers::Tokenizer;
use crate::tokens::count_text_tokens;
-use crate::tokens::count_text_tokens_with_fallback;
use crate::vecdb::vdb_structs::SplitResult;
@@ -17,28 +14,8 @@ pub fn official_text_hashing_function(s: &str) -> String {
}
-fn split_line_if_needed(line: &str, tokenizer: Option>, tokens_limit: usize) -> Vec {
- if let Some(tokenizer) = tokenizer {
- tokenizer.encode(line, false).map_or_else(
- |_| split_without_tokenizer(line, tokens_limit),
- |tokens| {
- let ids = tokens.get_ids();
- if ids.len() <= tokens_limit {
- vec![line.to_string()]
- } else {
- ids.chunks(tokens_limit)
- .filter_map(|chunk| tokenizer.decode(chunk, true).ok())
- .collect()
- }
- }
- )
- } else {
- split_without_tokenizer(line, tokens_limit)
- }
-}
-
-fn split_without_tokenizer(line: &str, tokens_limit: usize) -> Vec {
- if count_text_tokens(None, line).is_ok_and(|tokens| tokens <= tokens_limit) {
+fn split_line_if_needed(line: &str, tokens_limit: usize) -> Vec {
+ if count_text_tokens(line) <= tokens_limit {
vec![line.to_string()]
} else {
Rope::from_str(line).chars()
@@ -49,14 +26,14 @@ fn split_without_tokenizer(line: &str, tokens_limit: usize) -> Vec {
}
}
-pub fn get_chunks(text: &String,
- file_path: &PathBuf,
- symbol_path: &String,
- top_bottom_rows: (usize, usize), // case with top comments
- tokenizer: Option>,
- tokens_limit: usize,
- intersection_lines: usize,
- use_symbol_range_always: bool, // use for skeleton case
+pub fn get_chunks(
+ text: &String,
+ file_path: &PathBuf,
+ symbol_path: &String,
+ top_bottom_rows: (usize, usize), // case with top comments
+ tokens_limit: usize,
+ intersection_lines: usize,
+ use_symbol_range_always: bool, // use for skeleton case
) -> Vec {
let (top_row, bottom_row) = top_bottom_rows;
let mut chunks: Vec = Vec::new();
@@ -69,13 +46,13 @@ pub fn get_chunks(text: &String,
let mut previous_start = line_idx;
while line_idx < lines.len() {
let line = lines[line_idx];
- let line_tok_n = count_text_tokens_with_fallback(tokenizer.clone(), line);
+ let line_tok_n = count_text_tokens(line);
if !accum.is_empty() && current_tok_n + line_tok_n > tokens_limit {
let current_line = accum.iter().map(|(line, _)| line).join("\n");
let start_line = if use_symbol_range_always { top_row as u64 } else { accum.front().unwrap().1 as u64 };
let end_line = if use_symbol_range_always { bottom_row as u64 } else { accum.back().unwrap().1 as u64 };
- for chunked_line in split_line_if_needed(¤t_line, tokenizer.clone(), tokens_limit) {
+ for chunked_line in split_line_if_needed(¤t_line, tokens_limit) {
chunks.push(SplitResult {
file_path: file_path.clone(),
window_text: chunked_line.clone(),
@@ -104,12 +81,12 @@ pub fn get_chunks(text: &String,
current_tok_n = 0;
while line_idx >= 0 {
let line = lines[line_idx as usize];
- let text_orig_tok_n = count_text_tokens_with_fallback(tokenizer.clone(), line);
+ let text_orig_tok_n = count_text_tokens(line);
if !accum.is_empty() && current_tok_n + text_orig_tok_n > tokens_limit {
let current_line = accum.iter().map(|(line, _)| line).join("\n");
let start_line = if use_symbol_range_always { top_row as u64 } else { accum.front().unwrap().1 as u64 };
let end_line = if use_symbol_range_always { bottom_row as u64 } else { accum.back().unwrap().1 as u64 };
- for chunked_line in split_line_if_needed(¤t_line, tokenizer.clone(), tokens_limit) {
+ for chunked_line in split_line_if_needed(¤t_line, tokens_limit) {
chunks.push(SplitResult {
file_path: file_path.clone(),
window_text: chunked_line.clone(),
@@ -133,7 +110,7 @@ pub fn get_chunks(text: &String,
let current_line = accum.iter().map(|(line, _)| line).join("\n");
let start_line = if use_symbol_range_always { top_row as u64 } else { accum.front().unwrap().1 as u64 };
let end_line = if use_symbol_range_always { bottom_row as u64 } else { accum.back().unwrap().1 as u64 };
- for chunked_line in split_line_if_needed(¤t_line, tokenizer.clone(), tokens_limit) {
+ for chunked_line in split_line_if_needed(¤t_line, tokens_limit) {
chunks.push(SplitResult {
file_path: file_path.clone(),
window_text: chunked_line.clone(),
@@ -152,35 +129,10 @@ pub fn get_chunks(text: &String,
mod tests {
use std::path::PathBuf;
use std::str::FromStr;
- use std::sync::Arc;
-
use crate::ast::chunk_utils::get_chunks;
- use crate::tokens::count_text_tokens;
- // use crate::vecdb::vdb_structs::SplitResult;
-
- const DUMMY_TOKENIZER: &str = include_str!("dummy_tokenizer.json");
- const PYTHON_CODE: &str = r#"def square_number(x):
- """
- This function takes a number and returns its square.
-
- Parameters:
- x (int): A number to be squared.
-
- Returns:
- int: The square of the input number.
- """
- return x**2"#;
-
- #[test]
- fn dummy_tokenizer_test() {
- let tokenizer = Arc::new(tokenizers::Tokenizer::from_str(DUMMY_TOKENIZER).unwrap());
- let text_orig_tok_n = count_text_tokens(Some(tokenizer.clone()), PYTHON_CODE).unwrap();
- assert_eq!(text_orig_tok_n, PYTHON_CODE.len());
- }
#[test]
fn simple_chunk_test_1_with_128_limit() {
- let tokenizer = Some(Arc::new(tokenizers::Tokenizer::from_str(DUMMY_TOKENIZER).unwrap()));
let orig = include_str!("../caps/mod.rs").to_string();
let token_limits = [10, 50, 100, 200, 300];
for &token_limit in &token_limits {
@@ -189,7 +141,6 @@ mod tests {
&PathBuf::from_str("/tmp/test.py").unwrap(),
&"".to_string(),
(0, 10),
- tokenizer.clone(),
token_limit, 2, false);
let mut not_present: Vec = orig.chars().collect();
let mut result = String::new();
diff --git a/refact-agent/engine/src/ast/file_splitter.rs b/refact-agent/engine/src/ast/file_splitter.rs
index ab5e28a44..6f762d8dd 100644
--- a/refact-agent/engine/src/ast/file_splitter.rs
+++ b/refact-agent/engine/src/ast/file_splitter.rs
@@ -1,7 +1,6 @@
use std::collections::HashMap;
use std::sync::Arc;
use itertools::Itertools;
-use tokenizers::Tokenizer;
use tokio::sync::RwLock;
use uuid::Uuid;
@@ -30,7 +29,6 @@ impl AstBasedFileSplitter {
pub async fn vectorization_split(
&self,
doc: &Document,
- tokenizer: Option>,
gcx: Arc>,
tokens_limit: usize,
) -> Result, String> {
@@ -43,7 +41,7 @@ impl AstBasedFileSplitter {
Ok(parser) => parser,
Err(_e) => {
// tracing::info!("cannot find a parser for {:?}, using simple file splitter: {}", crate::nicer_logs::last_n_chars(&path.display().to_string(), 30), e.message);
- return self.fallback_file_splitter.vectorization_split(&doc, tokenizer.clone(), tokens_limit, gcx.clone()).await;
+ return self.fallback_file_splitter.vectorization_split(&doc, tokens_limit, gcx.clone()).await;
}
};
@@ -62,7 +60,7 @@ impl AstBasedFileSplitter {
Ok(x) => x,
Err(e) => {
tracing::info!("lowlevel_file_markup failed for {:?}, using simple file splitter: {}", crate::nicer_logs::last_n_chars(&path.display().to_string(), 30), e);
- return self.fallback_file_splitter.vectorization_split(&doc, tokenizer.clone(), tokens_limit, gcx.clone()).await;
+ return self.fallback_file_splitter.vectorization_split(&doc, tokens_limit, gcx.clone()).await;
}
};
@@ -82,9 +80,9 @@ impl AstBasedFileSplitter {
let top_row = unused_symbols_cluster_accumulator_.first().unwrap().full_range.start_point.row;
let bottom_row = unused_symbols_cluster_accumulator_.last().unwrap().full_range.end_point.row;
let content = doc_lines[top_row..bottom_row + 1].join("\n");
- let chunks__ = crate::ast::chunk_utils::get_chunks(&content, &path, &"".to_string(),
- (top_row, bottom_row),
- tokenizer.clone(), tokens_limit, LINES_OVERLAP, false);
+ let chunks__ = crate::ast::chunk_utils::get_chunks(
+ &content, &path, &"".to_string(), (top_row, bottom_row), tokens_limit, LINES_OVERLAP, false,
+ );
chunks_.extend(chunks__);
unused_symbols_cluster_accumulator_.clear();
}
@@ -121,10 +119,10 @@ impl AstBasedFileSplitter {
if let Some(children) = guid_to_children.get(&symbol.guid) {
if !children.is_empty() {
let skeleton_line = formatter.make_skeleton(&symbol, &doc_text, &guid_to_children, &guid_to_info);
- let chunks_ = crate::ast::chunk_utils::get_chunks(&skeleton_line, &symbol.file_path,
- &symbol.symbol_path,
- (symbol.full_range.start_point.row, symbol.full_range.end_point.row),
- tokenizer.clone(), tokens_limit, LINES_OVERLAP, true);
+ let chunks_ = crate::ast::chunk_utils::get_chunks(
+ &skeleton_line, &symbol.file_path, &symbol.symbol_path,
+ (symbol.full_range.start_point.row, symbol.full_range.end_point.row), tokens_limit, LINES_OVERLAP, true
+ );
chunks.extend(chunks_);
}
}
@@ -132,8 +130,9 @@ impl AstBasedFileSplitter {
let (declaration, top_bottom_rows) = formatter.get_declaration_with_comments(&symbol, &doc_text, &guid_to_children, &guid_to_info);
if !declaration.is_empty() {
- let chunks_ = crate::ast::chunk_utils::get_chunks(&declaration, &symbol.file_path,
- &symbol.symbol_path, top_bottom_rows, tokenizer.clone(), tokens_limit, LINES_OVERLAP, true);
+ let chunks_ = crate::ast::chunk_utils::get_chunks(
+ &declaration, &symbol.file_path, &symbol.symbol_path, top_bottom_rows, tokens_limit, LINES_OVERLAP, true
+ );
chunks.extend(chunks_);
}
}
diff --git a/refact-agent/engine/src/at_commands/at_commands.rs b/refact-agent/engine/src/at_commands/at_commands.rs
index fdd0b46e7..53b7e2598 100644
--- a/refact-agent/engine/src/at_commands/at_commands.rs
+++ b/refact-agent/engine/src/at_commands/at_commands.rs
@@ -1,7 +1,6 @@
use indexmap::IndexMap;
use std::collections::HashMap;
use std::sync::Arc;
-use tokio::sync::mpsc;
use async_trait::async_trait;
use tokio::sync::Mutex as AMutex;
@@ -14,7 +13,6 @@ use crate::at_commands::at_file::AtFile;
use crate::at_commands::at_ast_definition::AtAstDefinition;
use crate::at_commands::at_ast_reference::AtAstReference;
use crate::at_commands::at_tree::AtTree;
-use crate::at_commands::at_web::AtWeb;
use crate::at_commands::execute_at::AtCommandMember;
@@ -29,15 +27,10 @@ pub struct AtCommandsContext {
pub pp_skeleton: bool,
pub correction_only_up_to_step: usize, // suppresses context_file messages, writes a correction message instead
pub chat_id: String,
- pub current_model: String,
pub should_execute_remotely: bool,
-
pub at_commands: HashMap>, // a copy from static constant
pub subchat_tool_parameters: IndexMap,
pub postprocess_parameters: PostprocessSettings,
-
- pub subchat_tx: Arc>>, // one and only supported format for now {"tool_call_id": xx, "subchat_id": xx, "add_message": {...}}
- pub subchat_rx: Arc>>,
}
impl AtCommandsContext {
@@ -49,9 +42,7 @@ impl AtCommandsContext {
messages: Vec,
chat_id: String,
should_execute_remotely: bool,
- current_model: String,
) -> Self {
- let (tx, rx) = mpsc::unbounded_channel::();
AtCommandsContext {
global_context: global_context.clone(),
n_ctx,
@@ -62,15 +53,10 @@ impl AtCommandsContext {
pp_skeleton: true,
correction_only_up_to_step: 0,
chat_id,
- current_model,
should_execute_remotely,
-
at_commands: at_commands_dict(global_context.clone()).await,
subchat_tool_parameters: IndexMap::new(),
postprocess_parameters: PostprocessSettings::new(),
-
- subchat_tx: Arc::new(AMutex::new(tx)),
- subchat_rx: Arc::new(AMutex::new(rx)),
}
}
}
@@ -78,7 +64,6 @@ impl AtCommandsContext {
#[async_trait]
pub trait AtCommand: Send + Sync {
fn params(&self) -> &Vec>;
- // returns (messages_for_postprocessing, text_on_clip)
async fn at_execute(&self, ccx: Arc>, cmd: &mut AtCommandMember, args: &mut Vec) -> Result<(Vec, String), String>;
fn depends_on(&self) -> Vec { vec![] } // "ast", "vecdb"
}
@@ -93,16 +78,10 @@ pub trait AtParam: Send + Sync {
pub async fn at_commands_dict(gcx: Arc>) -> HashMap> {
let at_commands_dict = HashMap::from([
("@file".to_string(), Arc::new(AtFile::new()) as Arc),
- // ("@file-search".to_string(), Arc::new(AtFileSearch::new()) as Arc),
("@definition".to_string(), Arc::new(AtAstDefinition::new()) as Arc),
("@references".to_string(), Arc::new(AtAstReference::new()) as Arc),
- // ("@local-notes-to-self".to_string(), Arc::new(AtLocalNotesToSelf::new()) as Arc),
("@tree".to_string(), Arc::new(AtTree::new()) as Arc),
- // ("@diff".to_string(), Arc::new(AtDiff::new()) as Arc),
- // ("@diff-rev".to_string(), Arc::new(AtDiffRev::new()) as Arc),
- ("@web".to_string(), Arc::new(AtWeb::new()) as Arc),
("@search".to_string(), Arc::new(crate::at_commands::at_search::AtSearch::new()) as Arc),
- ("@knowledge-load".to_string(), Arc::new(crate::at_commands::at_knowledge::AtLoadKnowledge::new()) as Arc),
]);
let (ast_on, vecdb_on, active_group_id) = {
diff --git a/refact-agent/engine/src/at_commands/at_knowledge.rs b/refact-agent/engine/src/at_commands/at_knowledge.rs
deleted file mode 100644
index 551872c09..000000000
--- a/refact-agent/engine/src/at_commands/at_knowledge.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use std::sync::Arc;
-use std::collections::HashSet;
-use async_trait::async_trait;
-use itertools::Itertools;
-use tokio::sync::Mutex as AMutex;
-
-use crate::at_commands::at_commands::{AtCommand, AtCommandsContext, AtParam};
-use crate::at_commands::execute_at::AtCommandMember;
-use crate::call_validation::{ChatMessage, ContextEnum};
-use crate::memories::memories_search;
-
-/// @knowledge-load command - loads knowledge entries by search key or memory ID
-pub struct AtLoadKnowledge {
- params: Vec>,
-}
-
-impl AtLoadKnowledge {
- pub fn new() -> Self {
- AtLoadKnowledge {
- params: vec![],
- }
- }
-}
-
-#[async_trait]
-impl AtCommand for AtLoadKnowledge {
- fn params(&self) -> &Vec> {
- &self.params
- }
-
- async fn at_execute(
- &self,
- ccx: Arc>,
- _cmd: &mut AtCommandMember,
- args: &mut Vec,
- ) -> Result<(Vec, String), String> {
- if args.is_empty() {
- return Err("Usage: @knowledge-load ".to_string());
- }
-
- let search_key = args.iter().map(|x| x.text.clone()).join(" ").to_string();
- let gcx = {
- let ccx_locked = ccx.lock().await;
- ccx_locked.global_context.clone()
- };
-
- let mem_top_n = 5;
- let memories = memories_search(gcx.clone(), &search_key, mem_top_n).await?;
- let mut seen_memids = HashSet::new();
- let unique_memories: Vec<_> = memories.into_iter()
- .filter(|m| seen_memids.insert(m.iknow_id.clone()))
- .collect();
- let mut results = String::new();
- for memory in unique_memories {
- let mut content = String::new();
- content.push_str(&format!("🗃️{}\n", memory.iknow_id));
- content.push_str(&memory.iknow_memory);
- results.push_str(&content);
- };
-
- let context = ContextEnum::ChatMessage(ChatMessage::new("plain_text".to_string(), results));
- Ok((vec![context], "".to_string()))
- }
-
- fn depends_on(&self) -> Vec {
- vec!["knowledge".to_string()]
- }
-}
diff --git a/refact-agent/engine/src/at_commands/at_tree.rs b/refact-agent/engine/src/at_commands/at_tree.rs
index 5dc03c725..cbfc3e493 100644
--- a/refact-agent/engine/src/at_commands/at_tree.rs
+++ b/refact-agent/engine/src/at_commands/at_tree.rs
@@ -29,12 +29,6 @@ impl AtTree {
#[derive(Debug, Clone)]
pub struct PathsHolderNodeArc(Arc>);
-impl PathsHolderNodeArc {
- pub fn read(&self) -> std::sync::RwLockReadGuard<'_, PathsHolderNode> {
- self.0.read().unwrap()
- }
-}
-
impl PartialEq for PathsHolderNodeArc {
fn eq(&self, other: &Self) -> bool {
self.0.read().unwrap().path == other.0.read().unwrap().path
@@ -49,67 +43,6 @@ pub struct PathsHolderNode {
depth: usize,
}
-impl PathsHolderNode {
- pub fn file_name(&self) -> String {
- self.path.file_name().unwrap_or_default().to_string_lossy().to_string()
- }
-
- pub fn child_paths(&self) -> &Vec {
- &self.child_paths
- }
-
- pub fn get_path(&self) -> &PathBuf {
- &self.path
- }
-}
-
-pub fn construct_tree_out_of_flat_list_of_paths(paths_from_anywhere: &Vec) -> Vec {
- let mut root_nodes: Vec = Vec::new();
- let mut nodes_map: HashMap = HashMap::new();
-
- for path in paths_from_anywhere {
- let components: Vec<_> = path.components().collect();
- let components_count = components.len();
-
- let mut current_path = PathBuf::new();
- let mut parent_node: Option = None;
-
- for (index, component) in components.into_iter().enumerate() {
- current_path.push(component);
-
- let is_last = index == components_count - 1;
- let depth = index;
- let node = nodes_map.entry(current_path.clone()).or_insert_with(|| {
- PathsHolderNodeArc(Arc::new(RwLock::new(
- PathsHolderNode {
- path: current_path.clone(),
- is_dir: !is_last,
- child_paths: Vec::new(),
- depth,
- }
- )))
- });
-
- if node.0.read().unwrap().depth != depth {
- node.0.write().unwrap().depth = depth;
- }
-
- if let Some(parent) = parent_node {
- if !parent.0.read().unwrap().child_paths.contains(node) {
- parent.0.write().unwrap().child_paths.push(node.clone());
- }
- } else {
- if !root_nodes.contains(node) {
- root_nodes.push(node.clone());
- }
- }
-
- parent_node = Some(node.clone());
- }
- }
- root_nodes
-}
-
pub struct TreeNode {
children: HashMap,
// NOTE: we can store here more info like depth, sub files count, etc.
diff --git a/refact-agent/engine/src/at_commands/at_web.rs b/refact-agent/engine/src/at_commands/at_web.rs
deleted file mode 100644
index 768ea1ab9..000000000
--- a/refact-agent/engine/src/at_commands/at_web.rs
+++ /dev/null
@@ -1,230 +0,0 @@
-use std::sync::Arc;
-use std::time::Duration;
-use tracing::info;
-
-use reqwest::Client;
-use async_trait::async_trait;
-use tokio::sync::Mutex as AMutex;
-use select::predicate::{Attr, Name};
-use html2text::render::text_renderer::{TaggedLine, TextDecorator};
-
-use crate::at_commands::at_commands::{AtCommand, AtCommandsContext, AtParam};
-use crate::at_commands::execute_at::AtCommandMember;
-use crate::call_validation::{ChatMessage, ContextEnum};
-
-
-pub struct AtWeb {
- pub params: Vec>,
-}
-
-impl AtWeb {
- pub fn new() -> Self {
- AtWeb {
- params: vec![],
- }
- }
-}
-
-#[async_trait]
-impl AtCommand for AtWeb {
- fn params(&self) -> &Vec> {
- &self.params
- }
-
- async fn at_execute(
- &self,
- ccx: Arc>,
- cmd: &mut AtCommandMember,
- args: &mut Vec,
- ) -> Result<(Vec, String), String> {
- let url = match args.get(0) {
- Some(x) => x.clone(),
- None => {
- cmd.ok = false; cmd.reason = Some("missing URL".to_string());
- args.clear();
- return Err("missing URL".to_string());
- }
- };
- args.truncate(1);
-
- let preview_cache = {
- let gcx = ccx.lock().await.global_context.clone();
- let gcx_read = gcx.read().await;
- gcx_read.at_commands_preview_cache.clone()
- };
- let text_from_cache = preview_cache.lock().await.get(&format!("@web:{}", url.text));
-
- let text = match text_from_cache {
- Some(text) => text,
- None => {
- let text = execute_at_web(&url.text).await.map_err(|e|format!("Failed to execute @web {}.\nError: {e}", url.text))?;
- preview_cache.lock().await.insert(format!("@web:{}", url.text), text.clone());
- text
- }
- };
-
- let message = ChatMessage::new(
- "plain_text".to_string(),
- text,
- );
-
- info!("executed @web {}", url.text);
- Ok((vec![ContextEnum::ChatMessage(message)], format!("[see text downloaded from {} above]", url.text)))
- }
-
- fn depends_on(&self) -> Vec {
- vec![]
- }
-}
-
-#[derive(Clone, Copy)]
-struct CustomTextConversion;
-
-impl TextDecorator for CustomTextConversion {
- type Annotation = ();
-
- fn decorate_link_start(&mut self, _url: &str) -> (String, Self::Annotation) {
- ("[".to_string(), ())
- }
-
- fn decorate_link_end(&mut self) -> String {
- "]".to_string()
- }
-
- fn decorate_em_start(&self) -> (String, Self::Annotation) {
- ("*".to_string(), ())
- }
-
- fn decorate_em_end(&self) -> String {
- "*".to_string()
- }
-
- fn decorate_strong_start(&self) -> (String, Self::Annotation) {
- ("**".to_string(), ())
- }
-
- fn decorate_strong_end(&self) -> String {
- "**".to_string()
- }
-
- fn decorate_strikeout_start(&self) -> (String, Self::Annotation) {
- ("".to_string(), ())
- }
-
- fn decorate_strikeout_end(&self) -> String {
- "".to_string()
- }
-
- fn decorate_code_start(&self) -> (String, Self::Annotation) {
- ("`".to_string(), ())
- }
-
- fn decorate_code_end(&self) -> String {
- "`".to_string()
- }
-
- fn decorate_preformat_first(&self) -> Self::Annotation {}
- fn decorate_preformat_cont(&self) -> Self::Annotation {}
-
- fn decorate_image(&mut self, _src: &str, title: &str) -> (String, Self::Annotation) {
- (format!("[{}]", title), ())
- }
-
- fn header_prefix(&self, level: usize) -> String {
- "#".repeat(level) + " "
- }
-
- fn quote_prefix(&self) -> String {
- "> ".to_string()
- }
-
- fn unordered_item_prefix(&self) -> String {
- "* ".to_string()
- }
-
- fn ordered_item_prefix(&self, i: i64) -> String {
- format!("{}. ", i)
- }
-
- fn make_subblock_decorator(&self) -> Self {
- *self
- }
-
- fn finalise(&mut self, _: Vec) -> Vec> {
- vec![]
- }
-}
-
-fn find_content(html: String) -> String {
- let document = select::document::Document::from(html.as_str());
- let content_ids = vec![
- "content",
- "I_content",
- "main-content",
- "main_content",
- "CONTENT",
- ];
- for id in content_ids {
- if let Some(node) = document.find(Attr("id", id)).next() {
- return node.html();
- }
- }
- if let Some(node) = document.find(Name("article")).next() {
- return node.html();
- }
- if let Some(node) = document.find(Name("main")).next() {
- return node.html();
- }
- html
-}
-
-async fn fetch_html(url: &str, timeout: Duration) -> Result {
- let client = Client::builder()
- .timeout(timeout)
- .build()
- .map_err(|e| e.to_string())?;
-
- let response = client.get(url)
- .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
- .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
- .header("Accept-Language", "en-US,en;q=0.5")
- .header("Connection", "keep-alive")
- .header("Upgrade-Insecure-Requests", "1")
- .header("Cache-Control", "max-age=0")
- .header("DNT", "1")
- .header("Referer", "https://www.google.com/")
- .send().await.map_err(|e| e.to_string())?;
-
- if !response.status().is_success() {
- return Err(format!("unable to fetch url: {}; status: {}", url, response.status()));
- }
- let body = response.text().await.map_err(|e| e.to_string())?;
- Ok(body)
-}
-
-pub async fn execute_at_web(url: &str) -> Result{
- let html = fetch_html(url, Duration::from_secs(5)).await?;
- let html = find_content(html);
-
- let text = html2text::config::with_decorator(CustomTextConversion)
- .string_from_read(&html.as_bytes()[..], 200)
- .map_err(|_| "Unable to convert html to text".to_string())?;
-
- Ok(text)
-}
-
-
-#[cfg(test)]
-mod tests {
- use tracing::warn;
- use super::*;
-
- #[tokio::test]
- async fn test_execute_at_web() {
- let url = "https://doc.rust-lang.org/book/ch03-04-comments.html";
- match execute_at_web(url).await {
- Ok(text) => info!("Test executed successfully:\n\n{text}"),
- Err(e) => warn!("Test failed with error: {e}"),
- }
- }
-}
diff --git a/refact-agent/engine/src/at_commands/execute_at.rs b/refact-agent/engine/src/at_commands/execute_at.rs
index 79430f3f1..b6aa9e343 100644
--- a/refact-agent/engine/src/at_commands/execute_at.rs
+++ b/refact-agent/engine/src/at_commands/execute_at.rs
@@ -2,14 +2,10 @@ use std::sync::Arc;
use tokio::sync::Mutex as AMutex;
use regex::Regex;
use serde_json::{json, Value};
-use tokenizers::Tokenizer;
use tracing::{info, warn};
use crate::at_commands::at_commands::{AtCommandsContext, AtParam, filter_only_context_file_from_context_tool};
use crate::call_validation::{ChatContent, ChatMessage, ContextEnum};
-use crate::http::http_post_json;
-use crate::http::routers::v1::at_commands::{CommandExecutePost, CommandExecuteResponse};
-use crate::integrations::docker::docker_container_manager::docker_container_get_host_lsp_port_to_connect;
use crate::postprocessing::pp_context_files::postprocess_context_files;
use crate::postprocessing::pp_plain_text::postprocess_plain_text;
use crate::scratchpads::scratchpad_utils::{HasRagResults, max_tokens_for_rag_chat};
@@ -20,7 +16,6 @@ pub const MIN_RAG_CONTEXT_LIMIT: usize = 256;
pub async fn run_at_commands_locally(
ccx: Arc>,
- tokenizer: Option>,
maxgen: usize,
mut original_messages: Vec,
stream_back_to_user: &mut HasRagResults,
@@ -66,7 +61,7 @@ pub async fn run_at_commands_locally(
continue;
}
let mut content = msg.content.content_text_only();
- let content_n_tokens = msg.content.count_tokens(tokenizer.clone(), &None).unwrap_or(0) as usize;
+ let content_n_tokens = msg.content.count_tokens(&None).unwrap_or(0) as usize;
let mut context_limit = reserve_for_context / messages_with_at.max(1);
context_limit = context_limit.saturating_sub(content_n_tokens);
@@ -114,7 +109,6 @@ pub async fn run_at_commands_locally(
let (pp_plain_text, non_used_plain) = postprocess_plain_text(
plain_text_messages,
- tokenizer.clone(),
tokens_limit_plain,
&None,
).await;
@@ -136,7 +130,6 @@ pub async fn run_at_commands_locally(
let post_processed = postprocess_context_files(
gcx.clone(),
&mut context_file_pp,
- tokenizer.clone(),
tokens_limit_files,
false,
&pp_settings,
@@ -167,47 +160,6 @@ pub async fn run_at_commands_locally(
(new_messages, any_context_produced)
}
-pub async fn run_at_commands_remotely(
- ccx: Arc>,
- model_id: &str,
- maxgen: usize,
- original_messages: Vec,
- stream_back_to_user: &mut HasRagResults,
-) -> Result<(Vec, bool), String> {
- let (gcx, n_ctx, subchat_tool_parameters, postprocess_parameters, chat_id) = {
- let ccx_locked = ccx.lock().await;
- (
- ccx_locked.global_context.clone(),
- ccx_locked.n_ctx,
- ccx_locked.subchat_tool_parameters.clone(),
- ccx_locked.postprocess_parameters.clone(),
- ccx_locked.chat_id.clone()
- )
- };
-
- let post = CommandExecutePost {
- messages: original_messages,
- n_ctx,
- maxgen,
- subchat_tool_parameters,
- postprocess_parameters,
- model_name: model_id.to_string(),
- chat_id: chat_id.clone(),
- };
-
- let port = docker_container_get_host_lsp_port_to_connect(gcx.clone(), &chat_id).await?;
- tracing::info!("run_at_commands_remotely: connecting to port {}", port);
-
- let url = format!("http://localhost:{port}/v1/at-command-execute");
- let response: CommandExecuteResponse = http_post_json(&url, &post).await?;
-
- for msg in response.messages_to_stream_back {
- stream_back_to_user.push_in_json(msg);
- }
-
- Ok((response.messages, response.any_context_produced))
-}
-
pub async fn correct_at_arg(
ccx: Arc>,
param: &Box,
diff --git a/refact-agent/engine/src/at_commands/mod.rs b/refact-agent/engine/src/at_commands/mod.rs
index 385b7bbe2..b056a4abb 100644
--- a/refact-agent/engine/src/at_commands/mod.rs
+++ b/refact-agent/engine/src/at_commands/mod.rs
@@ -3,7 +3,5 @@ pub mod at_ast_definition;
pub mod at_ast_reference;
pub mod at_commands;
pub mod at_file;
-pub mod at_web;
pub mod at_tree;
pub mod at_search;
-pub mod at_knowledge;
diff --git a/refact-agent/engine/src/background_tasks.rs b/refact-agent/engine/src/background_tasks.rs
index 4e871b3ec..5ea2c44a9 100644
--- a/refact-agent/engine/src/background_tasks.rs
+++ b/refact-agent/engine/src/background_tasks.rs
@@ -1,5 +1,4 @@
use std::iter::IntoIterator;
-use std::path::PathBuf;
use std::sync::Arc;
use std::vec;
use tokio::sync::RwLock as ARwLock;
@@ -39,14 +38,11 @@ impl BackgroundTasksHolder {
}
}
-pub async fn start_background_tasks(gcx: Arc>, config_dir: &PathBuf) -> BackgroundTasksHolder {
+pub async fn start_background_tasks(gcx: Arc>) -> BackgroundTasksHolder {
let mut bg = BackgroundTasksHolder::new(vec![
tokio::spawn(crate::files_in_workspace::files_in_workspace_init_task(gcx.clone())),
- tokio::spawn(crate::telemetry::basic_transmit::telemetry_background_task(gcx.clone())),
- tokio::spawn(crate::snippets_transmit::tele_snip_background_task(gcx.clone())),
tokio::spawn(crate::vecdb::vdb_highlev::vecdb_background_reload(gcx.clone())), // this in turn can create global_context::vec_db
tokio::spawn(crate::integrations::sessions::remove_expired_sessions_background_task(gcx.clone())),
- tokio::spawn(crate::memories::memories_migration(gcx.clone(), config_dir.clone())),
tokio::spawn(crate::git::cleanup::git_shadow_cleanup_background_task(gcx.clone())),
tokio::spawn(crate::cloud::threads_sub::watch_threads_subscription(gcx.clone())),
]);
diff --git a/refact-agent/engine/src/basic_utils.rs b/refact-agent/engine/src/basic_utils.rs
new file mode 100644
index 000000000..f545076da
--- /dev/null
+++ b/refact-agent/engine/src/basic_utils.rs
@@ -0,0 +1,11 @@
+use rand::distributions::Alphanumeric;
+use rand::{thread_rng, Rng};
+
+pub fn generate_random_hash(length: usize) -> String {
+ thread_rng()
+ .sample_iter(&Alphanumeric)
+ .take(length)
+ .map(char::from)
+ .collect()
+}
+
diff --git a/refact-agent/engine/src/call_validation.rs b/refact-agent/engine/src/call_validation.rs
index 03b625e89..1d5395590 100644
--- a/refact-agent/engine/src/call_validation.rs
+++ b/refact-agent/engine/src/call_validation.rs
@@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::hash::Hash;
use axum::http::StatusCode;
-use indexmap::IndexMap;
use ropey::Rope;
use crate::custom_error::ScratchError;
@@ -159,13 +158,6 @@ impl Default for ChatContent {
}
}
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
-pub struct ChatUsage {
- pub prompt_tokens: usize,
- pub completion_tokens: usize,
- pub total_tokens: usize, // TODO: remove (can produce self-contradictory data when prompt+completion != total)
-}
-
#[derive(Debug, Serialize, Clone, Default)]
pub struct ChatMessage {
pub role: String,
@@ -178,8 +170,6 @@ pub struct ChatMessage {
pub tool_call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_failed: Option,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub usage: Option,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checkpoints: Vec,
#[serde(default, skip_serializing_if="Option::is_none")]
@@ -210,10 +200,6 @@ impl Default for ChatModelType {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SubchatParameters {
- #[serde(default)]
- pub subchat_model_type: ChatModelType,
- #[serde(default)]
- pub subchat_model: String,
pub subchat_n_ctx: usize,
#[serde(default)]
pub subchat_tokens_for_rag: usize,
@@ -225,83 +211,6 @@ pub struct SubchatParameters {
pub subchat_reasoning_effort: Option,
}
-#[derive(Debug, Deserialize, Clone, Default)]
-pub struct ChatPost {
- pub messages: Vec,
- #[serde(default)]
- pub parameters: SamplingParameters,
- #[serde(default)]
- pub model: String,
- pub stream: Option,
- pub temperature: Option,
- #[serde(default)]
- pub max_tokens: Option,
- #[serde(default)]
- pub increase_max_tokens: bool,
- #[serde(default)]
- pub n: Option,
- #[serde(default)]
- pub tool_choice: Option,
- #[serde(default)]
- pub checkpoints_enabled: bool,
- #[serde(default)]
- pub only_deterministic_messages: bool, // means don't sample from the model
- #[serde(default)]
- pub subchat_tool_parameters: IndexMap, // tool_name: {model, allowed_context, temperature}
- #[serde(default = "PostprocessSettings::new")]
- pub postprocess_parameters: PostprocessSettings,
- #[serde(default)]
- pub meta: ChatMeta,
- #[serde(default)]
- pub style: Option,
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
-pub struct ChatMeta {
- #[serde(default)]
- pub chat_id: String,
- #[serde(default)]
- pub request_attempt_id: String,
- #[serde(default)]
- pub chat_remote: bool,
- #[serde(default)]
- pub chat_mode: ChatMode,
- #[serde(default)]
- pub current_config_file: String,
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy)]
-#[allow(non_camel_case_types)]
-pub enum ChatMode {
- NO_TOOLS,
- EXPLORE,
- AGENT,
- CONFIGURE,
- PROJECT_SUMMARY,
-}
-
-impl ChatMode {
- pub fn supports_checkpoints(self) -> bool {
- match self {
- ChatMode::NO_TOOLS => false,
- ChatMode::AGENT | ChatMode::CONFIGURE | ChatMode::PROJECT_SUMMARY | ChatMode::EXPLORE => true,
- }
- }
-
- pub fn is_agentic(self) -> bool {
- match self {
- ChatMode::AGENT => true,
- ChatMode::NO_TOOLS | ChatMode::EXPLORE | ChatMode::CONFIGURE |
- ChatMode::PROJECT_SUMMARY => false,
- }
- }
-}
-
-impl Default for ChatMode {
- fn default() -> Self {
- ChatMode::NO_TOOLS
- }
-}
fn default_true() -> bool {
true
diff --git a/refact-agent/engine/src/caps/caps.rs b/refact-agent/engine/src/caps/caps.rs
index 433586279..05ec11cee 100644
--- a/refact-agent/engine/src/caps/caps.rs
+++ b/refact-agent/engine/src/caps/caps.rs
@@ -400,18 +400,6 @@ pub fn resolve_model<'a, T>(
).cloned().ok_or(format!("Model '{}' not found. Server has the following models: {:?}", model_id, models.keys()))
}
-pub fn resolve_chat_model<'a>(
- caps: Arc,
- requested_model_id: &str,
-) -> Result, String> {
- let model_id = if !requested_model_id.is_empty() {
- requested_model_id
- } else {
- &caps.defaults.chat_default_model
- };
- resolve_model(&caps.chat_models, model_id)
-}
-
pub fn resolve_completion_model<'a>(
caps: Arc,
requested_model_id: &str,
@@ -437,7 +425,3 @@ pub fn resolve_completion_model<'a>(
Err(err) => Err(err),
}
}
-
-pub fn is_cloud_model(model_id: &str) -> bool {
- model_id.starts_with("refact/")
-}
diff --git a/refact-agent/engine/src/cloud/cloud_tools_req.rs b/refact-agent/engine/src/cloud/cloud_tools_req.rs
new file mode 100644
index 000000000..f7016a2eb
--- /dev/null
+++ b/refact-agent/engine/src/cloud/cloud_tools_req.rs
@@ -0,0 +1,65 @@
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+use tracing::info;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CloudTool {
+ pub owner_fuser_id: Option,
+ pub located_fgroup_id: Option,
+ pub ctool_id: String,
+ pub ctool_name: String,
+ pub ctool_description: String,
+ pub ctool_confirmed_exists_ts: Option,
+ pub ctool_parameters: Value,
+}
+
+impl CloudTool {
+ pub fn into_openai_style(self) -> Value {
+ json!({
+ "type": "function",
+ "function": {
+ "name": self.ctool_name,
+ "description": self.ctool_description,
+ "parameters": self.ctool_parameters,
+ }
+ })
+ }
+}
+
+pub async fn get_cloud_tools(
+ cmd_address_url: &str,
+ api_key: &str,
+ located_fgroup_id: &str,
+) -> Result, String> {
+ use crate::cloud::graphql_client::{execute_graphql, GraphQLRequestConfig};
+
+ let query = r#"
+ query GetCloudTools($located_fgroup_id: String!) {
+ cloud_tools_list(located_fgroup_id: $located_fgroup_id, include_offline: true) {
+ owner_fuser_id
+ located_fgroup_id
+ ctool_id
+ ctool_name
+ ctool_description
+ ctool_confirmed_exists_ts
+ ctool_parameters
+ }
+ }
+ "#;
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
+
+ info!("get_cloud_tools: address={}, located_fgroup_id={}", config.address, located_fgroup_id);
+ execute_graphql::, _>(
+ config,
+ query,
+ json!({"located_fgroup_id": located_fgroup_id}),
+ "cloud_tools_list"
+ )
+ .await
+ .map_err(|e| e.to_string())
+}
diff --git a/refact-agent/engine/src/cloud/experts_req.rs b/refact-agent/engine/src/cloud/experts_req.rs
index 74fb9ac8b..4a698e393 100644
--- a/refact-agent/engine/src/cloud/experts_req.rs
+++ b/refact-agent/engine/src/cloud/experts_req.rs
@@ -1,18 +1,15 @@
use log::error;
use regex::Regex;
-use reqwest::Client;
use serde::{Deserialize, Serialize};
-use serde_json::{json, Value};
-use std::sync::Arc;
-use tokio::sync::RwLock as ARwLock;
-
-use crate::global_context::GlobalContext;
+use serde_json::json;
+use tracing::info;
#[derive(Debug, Serialize, Deserialize)]
pub struct Expert {
pub owner_fuser_id: Option,
pub owner_shared: bool,
pub located_fgroup_id: Option,
+ pub fexp_id: String,
pub fexp_name: String,
pub fexp_system_prompt: String,
pub fexp_python_kernel: String,
@@ -60,17 +57,19 @@ impl Expert {
}
pub async fn get_expert(
- gcx: Arc>,
- expert_name: &str
+ cmd_address_url: &str,
+ api_key: &str,
+ fexp_id: &str
) -> Result {
- let client = Client::new();
- let api_key = gcx.read().await.cmdline.api_key.clone();
+ use crate::cloud::graphql_client::{execute_graphql, GraphQLRequestConfig};
+
let query = r#"
query GetExpert($id: String!) {
expert_get(id: $id) {
owner_fuser_id
owner_shared
located_fgroup_id
+ fexp_id
fexp_name
fexp_system_prompt
fexp_python_kernel
@@ -79,52 +78,69 @@ pub async fn get_expert(
}
}
"#;
- let response = client
- .post(&crate::constants::GRAPHQL_URL.to_string())
- .header("Authorization", format!("Bearer {}", api_key))
- .header("Content-Type", "application/json")
- .json(&json!({
- "query": query,
- "variables": {
- "id": expert_name
- }
- }))
- .send()
- .await
- .map_err(|e| format!("Failed to send GraphQL request: {}", e))?;
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
- if response.status().is_success() {
- let response_body = response
- .text()
- .await
- .map_err(|e| format!("Failed to read response body: {}", e))?;
- let response_json: Value = serde_json::from_str(&response_body)
- .map_err(|e| format!("Failed to parse response JSON: {}", e))?;
- if let Some(errors) = response_json.get("errors") {
- let error_msg = errors.to_string();
- error!("GraphQL error: {}", error_msg);
- return Err(format!("GraphQL error: {}", error_msg));
- }
- if let Some(data) = response_json.get("data") {
- if let Some(expert_value) = data.get("expert_get") {
- let expert: Expert = serde_json::from_value(expert_value.clone())
- .map_err(|e| format!("Failed to parse expert: {}", e))?;
- return Ok(expert);
- }
+ info!("get_expert: address={}, fexp_id={}", config.address, fexp_id);
+ execute_graphql::(
+ config,
+ query,
+ json!({"id": fexp_id}),
+ "expert_get"
+ )
+ .await
+ .map_err(|e| e.to_string())
+}
+
+pub async fn expert_choice_consequences(
+ cmd_address_url: &str,
+ api_key: &str,
+ fexp_id: &str,
+ fgroup_id: &str,
+) -> Result {
+ use crate::cloud::graphql_client::{execute_graphql, GraphQLRequestConfig};
+
+ #[derive(Deserialize, Debug)]
+ struct ModelInfo {
+ provm_name: String,
+ }
+
+ let query = r#"
+ query GetExpertModel($fexp_id: String!, $inside_fgroup_id: String!) {
+ expert_choice_consequences(fexp_id: $fexp_id, inside_fgroup_id: $inside_fgroup_id) {
+ provm_name
}
- Err(format!(
- "Expert with name '{}' not found or unexpected response format: {}",
- expert_name, response_body
- ))
- } else {
- let status = response.status();
- let error_text = response
- .text()
- .await
- .unwrap_or_else(|_| "Unknown error".to_string());
- Err(format!(
- "Failed to get expert with name {}: HTTP status {}, error: {}",
- expert_name, status, error_text
- ))
}
+ "#;
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
+
+ let variables = json!({
+ "fexp_id": fexp_id,
+ "inside_fgroup_id": fgroup_id
+ });
+
+ info!("expert_choice_consequences: address={}, fexp_id={}, inside_fgroup_id={}", config.address, fexp_id, fgroup_id);
+ let result: Vec = execute_graphql(
+ config,
+ query,
+ variables,
+ "expert_choice_consequences"
+ )
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if result.is_empty() {
+ return Err(format!("No models found for the expert with name {}", fexp_id));
+ }
+
+ Ok(result[0].provm_name.clone())
}
diff --git a/refact-agent/engine/src/cloud/graphql_client.rs b/refact-agent/engine/src/cloud/graphql_client.rs
new file mode 100644
index 000000000..ee4f80ffb
--- /dev/null
+++ b/refact-agent/engine/src/cloud/graphql_client.rs
@@ -0,0 +1,338 @@
+use std::fmt::{Display, Formatter};
+use std::fmt::Debug;
+use std::collections::HashMap;
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use reqwest::Client;
+use serde_json::{Value, json};
+use log::error;
+use crate::constants::get_graphql_url;
+
+/// Configuration for GraphQL requests
+pub struct GraphQLRequestConfig {
+ pub address: String,
+ pub api_key: String,
+ pub user_agent: Option,
+ pub additional_headers: Option>,
+}
+
+impl Default for GraphQLRequestConfig {
+ fn default() -> Self {
+ Self {
+ address: String::new(),
+ api_key: String::new(),
+ user_agent: Some("refact-lsp".to_string()),
+ additional_headers: None,
+ }
+ }
+}
+
+/// Generic error type for GraphQL operations
+#[derive(Debug)]
+pub enum GraphQLError {
+ Network(reqwest::Error),
+ Json(serde_json::Error),
+ GraphQL(String),
+ Response {
+ status: reqwest::StatusCode,
+ message: String
+ },
+ UnexpectedFormat(String),
+ DataNotFound(String),
+}
+
+impl Display for GraphQLError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ GraphQLError::Network(err) => write!(f, "Network error: {}", err),
+ GraphQLError::Json(err) => write!(f, "JSON error: {}", err),
+ GraphQLError::GraphQL(err) => write!(f, "GraphQL error: {}", err),
+ GraphQLError::Response { status, message } => {
+ write!(f, "Response error: HTTP status {}, error: {}", status, message)
+ }
+ GraphQLError::UnexpectedFormat(err) => write!(f, "Unexpected response format: {}", err),
+ GraphQLError::DataNotFound(err) => write!(f, "Data not found in response: {}", err),
+ }
+ }
+}
+
+impl std::error::Error for GraphQLError {}
+
+impl From for GraphQLError {
+ fn from(err: reqwest::Error) -> Self {
+ GraphQLError::Network(err)
+ }
+}
+
+impl From for GraphQLError {
+ fn from(err: serde_json::Error) -> Self {
+ GraphQLError::Json(err)
+ }
+}
+
+/// Type alias for GraphQL results
+pub type GraphQLResult = Result;
+
+/// Execute a GraphQL operation and return the deserialized result
+pub async fn execute_graphql(
+ config: GraphQLRequestConfig,
+ operation: &str,
+ variables: V,
+ result_path: &str,
+) -> GraphQLResult
+where
+ T: DeserializeOwned + Debug,
+ V: Serialize,
+{
+ let client = Client::new();
+
+ let mut request_builder = client
+ .post(&get_graphql_url(&config.address))
+ .header("Authorization", format!("Bearer {}", config.api_key))
+ .header("Content-Type", "application/json");
+
+ if let Some(user_agent) = config.user_agent {
+ request_builder = request_builder.header("User-Agent", user_agent);
+ }
+
+ if let Some(headers) = config.additional_headers {
+ for (name, value) in headers {
+ request_builder = request_builder.header(name, value);
+ }
+ }
+
+ let request_body = json!({
+ "query": operation,
+ "variables": variables
+ });
+
+ let response = request_builder
+ .json(&request_body)
+ .send()
+ .await
+ .map_err(GraphQLError::Network)?;
+
+ if response.status().is_success() {
+ let response_body = response
+ .text()
+ .await
+ .map_err(|e| GraphQLError::Network(e))?;
+
+ let response_json: Value = serde_json::from_str(&response_body)
+ .map_err(GraphQLError::Json)?;
+
+ if let Some(errors) = response_json.get("errors") {
+ let error_msg = errors.to_string();
+ error!("GraphQL error: {}", error_msg);
+ return Err(GraphQLError::GraphQL(error_msg));
+ }
+
+ if let Some(data) = response_json.get("data") {
+ if let Some(result_value) = data.get(result_path) {
+ if result_value.is_null() {
+ return Err(GraphQLError::DataNotFound(format!(
+ "Result at path '{}' is null", result_path
+ )));
+ }
+
+ let result = serde_json::from_value(result_value.clone())
+ .map_err(|e| GraphQLError::Json(e))?;
+ return Ok(result);
+ }
+
+ return Err(GraphQLError::DataNotFound(format!(
+ "Result path '{}' not found in response data", result_path
+ )));
+ }
+
+ Err(GraphQLError::UnexpectedFormat(format!(
+ "Unexpected response format: {}",
+ response_body
+ )))
+ } else {
+ let status = response.status();
+ let error_text = response
+ .text()
+ .await
+ .unwrap_or_else(|_| "Unknown error".to_string());
+
+ Err(GraphQLError::Response {
+ status,
+ message: error_text,
+ })
+ }
+}
+
+/// Execute a GraphQL operation that doesn't return a specific result
+pub async fn execute_graphql_no_result(
+ config: GraphQLRequestConfig,
+ operation: &str,
+ variables: V,
+ result_path: &str,
+) -> GraphQLResult<()>
+where
+ V: Serialize,
+{
+ let client = Client::new();
+
+ let mut request_builder = client
+ .post(&get_graphql_url(&config.address))
+ .header("Authorization", format!("Bearer {}", config.api_key))
+ .header("Content-Type", "application/json");
+
+ if let Some(user_agent) = config.user_agent {
+ request_builder = request_builder.header("User-Agent", user_agent);
+ }
+
+ if let Some(headers) = config.additional_headers {
+ for (name, value) in headers {
+ request_builder = request_builder.header(name, value);
+ }
+ }
+
+ let request_body = json!({
+ "query": operation,
+ "variables": variables
+ });
+
+ let response = request_builder
+ .json(&request_body)
+ .send()
+ .await
+ .map_err(GraphQLError::Network)?;
+
+ if response.status().is_success() {
+ let response_body = response
+ .text()
+ .await
+ .map_err(|e| GraphQLError::Network(e))?;
+
+ let response_json: Value = serde_json::from_str(&response_body)
+ .map_err(GraphQLError::Json)?;
+
+ if let Some(errors) = response_json.get("errors") {
+ let error_msg = errors.to_string();
+ error!("GraphQL error: {}", error_msg);
+ return Err(GraphQLError::GraphQL(error_msg));
+ }
+
+ if let Some(data) = response_json.get("data") {
+ if data.get(result_path).is_some() {
+ return Ok(());
+ }
+
+ return Err(GraphQLError::DataNotFound(format!(
+ "Result path '{}' not found in response data", result_path
+ )));
+ }
+
+ Err(GraphQLError::UnexpectedFormat(format!(
+ "Unexpected response format: {}",
+ response_body
+ )))
+ } else {
+ let status = response.status();
+ let error_text = response
+ .text()
+ .await
+ .unwrap_or_else(|_| "Unknown error".to_string());
+
+ Err(GraphQLError::Response {
+ status,
+ message: error_text,
+ })
+ }
+}
+
+/// Execute a GraphQL operation that returns a boolean success indicator
+pub async fn execute_graphql_bool_result(
+ config: GraphQLRequestConfig,
+ operation: &str,
+ variables: V,
+ result_path: &str,
+) -> GraphQLResult
+where
+ V: Serialize,
+{
+ let client = Client::new();
+
+ let mut request_builder = client
+ .post(&get_graphql_url(&config.address))
+ .header("Authorization", format!("Bearer {}", config.api_key))
+ .header("Content-Type", "application/json");
+
+ if let Some(user_agent) = config.user_agent {
+ request_builder = request_builder.header("User-Agent", user_agent);
+ }
+
+ if let Some(headers) = config.additional_headers {
+ for (name, value) in headers {
+ request_builder = request_builder.header(name, value);
+ }
+ }
+
+ let request_body = json!({
+ "query": operation,
+ "variables": variables
+ });
+
+ let response = request_builder
+ .json(&request_body)
+ .send()
+ .await
+ .map_err(GraphQLError::Network)?;
+
+ if response.status().is_success() {
+ let response_body = response
+ .text()
+ .await
+ .map_err(|e| GraphQLError::Network(e))?;
+
+ let response_json: Value = serde_json::from_str(&response_body)
+ .map_err(GraphQLError::Json)?;
+
+ if let Some(errors) = response_json.get("errors") {
+ let error_msg = errors.to_string();
+ error!("GraphQL error: {}", error_msg);
+ return Err(GraphQLError::GraphQL(error_msg));
+ }
+
+ if let Some(data) = response_json.get("data") {
+ if let Some(result_value) = data.get(result_path) {
+ if let Some(bool_value) = result_value.as_bool() {
+ return Ok(bool_value);
+ }
+
+ return Err(GraphQLError::UnexpectedFormat(format!(
+ "Expected boolean value at path '{}', got: {:?}",
+ result_path, result_value
+ )));
+ }
+
+ return Err(GraphQLError::DataNotFound(format!(
+ "Result path '{}' not found in response data", result_path
+ )));
+ }
+
+ Err(GraphQLError::UnexpectedFormat(format!(
+ "Unexpected response format: {}",
+ response_body
+ )))
+ } else {
+ let status = response.status();
+ let error_text = response
+ .text()
+ .await
+ .unwrap_or_else(|_| "Unknown error".to_string());
+
+ Err(GraphQLError::Response {
+ status,
+ message: error_text,
+ })
+ }
+}
+
+/// Convert GraphQLError to string error message for backward compatibility
+pub fn graphql_error_to_string(error: GraphQLError) -> String {
+ error.to_string()
+}
diff --git a/refact-agent/engine/src/cloud/memories_req.rs b/refact-agent/engine/src/cloud/memories_req.rs
new file mode 100644
index 000000000..24a2c5bc6
--- /dev/null
+++ b/refact-agent/engine/src/cloud/memories_req.rs
@@ -0,0 +1,108 @@
+use std::sync::Arc;
+use serde::{Deserialize, Serialize};
+use serde_json::json;
+use tokio::sync::RwLock as ARwLock;
+use tracing::info;
+use crate::global_context::GlobalContext;
+use crate::cloud::graphql_client::{execute_graphql, GraphQLRequestConfig};
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct MemoRecord {
+ pub iknow_id: String,
+ pub iknow_tags: Vec,
+ pub iknow_memory: String,
+}
+
+pub async fn memories_add(
+ gcx: Arc>,
+ m_type: &str,
+ m_memory: &str,
+) -> Result<(), String> {
+ let (cmd_address_url, api_key) = {
+ let gcx_read = gcx.read().await;
+ (gcx_read.cmdline.address_url.clone(), gcx_read.cmdline.api_key.clone())
+ };
+ let active_group_id = gcx.read().await.active_group_id.clone()
+ .ok_or("active_group_id must be set")?;
+
+ let query = r#"
+ mutation CreateKnowledgeItem($input: FKnowledgeItemInput!) {
+ knowledge_item_create(input: $input) {
+ iknow_id
+ }
+ }
+ "#;
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key,
+ ..Default::default()
+ };
+
+ let variables = json!({
+ "input": {
+ "iknow_memory": m_memory,
+ "located_fgroup_id": active_group_id,
+ "iknow_is_core": false,
+ "iknow_tags": vec![m_type.to_string()],
+ "owner_shared": false
+ }
+ });
+
+ info!("memories_add: address={}, iknow_memory={}, located_fgroup_id={}, iknow_is_core={}, iknow_tags={}, owner_shared={}",
+ config.address, m_memory, active_group_id, false, m_type, false
+ );
+ execute_graphql::(
+ config,
+ query,
+ variables,
+ "knowledge_item_create"
+ )
+ .await
+ .map_err(|e| e.to_string())?;
+ info!("Successfully added memory to remote server");
+
+ Ok(())
+}
+
+pub async fn memories_get_core(
+ gcx: Arc>
+) -> Result, String> {
+ let (cmd_address_url, api_key) = {
+ let gcx_read = gcx.read().await;
+ (gcx_read.cmdline.address_url.clone(), gcx_read.cmdline.api_key.clone())
+ };
+ let active_group_id = gcx.read().await.active_group_id.clone()
+ .ok_or("active_group_id must be set")?;
+
+ let query = r#"
+ query KnowledgeSearch($fgroup_id: String!) {
+ knowledge_get_cores(fgroup_id: $fgroup_id) {
+ iknow_id
+ iknow_memory
+ iknow_tags
+ }
+ }
+ "#;
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url,
+ api_key,
+ ..Default::default()
+ };
+
+ let variables = json!({
+ "fgroup_id": active_group_id
+ });
+ info!("memories_get_core: address={}, fgroup_id={}", config.address, active_group_id);
+ let memories: Vec = execute_graphql(
+ config,
+ query,
+ variables,
+ "knowledge_get_cores"
+ )
+ .await
+ .map_err(|e| e.to_string())?;
+
+ Ok(memories)
+}
diff --git a/refact-agent/engine/src/cloud/messages_req.rs b/refact-agent/engine/src/cloud/messages_req.rs
index 04f16ee30..73eab1629 100644
--- a/refact-agent/engine/src/cloud/messages_req.rs
+++ b/refact-agent/engine/src/cloud/messages_req.rs
@@ -1,20 +1,16 @@
-use crate::call_validation::{ChatContent, ChatMessage, ChatToolCall, ChatUsage, DiffChunk};
-use crate::global_context::GlobalContext;
-use log::error;
-use reqwest::Client;
+use crate::call_validation::{ChatContent, ChatMessage, ChatToolCall, DiffChunk};
+use crate::cloud::graphql_client::{execute_graphql, execute_graphql_no_result, GraphQLRequestConfig, graphql_error_to_string};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
-use std::sync::Arc;
use itertools::Itertools;
-use tokio::sync::RwLock as ARwLock;
use tracing::warn;
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ThreadMessage {
pub ftm_belongs_to_ft_id: String,
- pub ftm_alt: i32,
- pub ftm_num: i32,
- pub ftm_prev_alt: i32,
+ pub ftm_alt: i64,
+ pub ftm_num: i64,
+ pub ftm_prev_alt: i64,
pub ftm_role: String,
pub ftm_content: Option,
pub ftm_tool_calls: Option,
@@ -22,15 +18,15 @@ pub struct ThreadMessage {
pub ftm_usage: Option,
pub ftm_created_ts: f64,
pub ftm_provenance: Value,
+ pub ftm_user_preferences: Option
}
pub async fn get_thread_messages(
- gcx: Arc>,
+ cmd_address_url: &str,
+ api_key: &str,
thread_id: &str,
- alt: i64,
+ alt: i64 ,
) -> Result, String> {
- let client = Client::new();
- let api_key = gcx.read().await.cmdline.api_key.clone();
let query = r#"
query GetThreadMessagesByAlt($thread_id: String!, $alt: Int!) {
thread_messages_list(
@@ -48,6 +44,7 @@ pub async fn get_thread_messages(
ftm_usage
ftm_created_ts
ftm_provenance
+ ftm_user_preferences
}
}
"#;
@@ -55,62 +52,38 @@ pub async fn get_thread_messages(
"thread_id": thread_id,
"alt": alt
});
- let response = client
- .post(&crate::constants::GRAPHQL_URL.to_string())
- .header("Authorization", format!("Bearer {}", api_key))
- .header("Content-Type", "application/json")
- .json(&json!({
- "query": query,
- "variables": variables
- }))
- .send()
- .await
- .map_err(|e| format!("Failed to send GraphQL request: {}", e))?;
- if response.status().is_success() {
- let response_body = response
- .text()
- .await
- .map_err(|e| format!("Failed to read response body: {}", e))?;
- let response_json: Value = serde_json::from_str(&response_body)
- .map_err(|e| format!("Failed to parse response JSON: {}", e))?;
- if let Some(errors) = response_json.get("errors") {
- let error_msg = errors.to_string();
- error!("GraphQL error: {}", error_msg);
- return Err(format!("GraphQL error: {}", error_msg));
- }
- if let Some(data) = response_json.get("data") {
- if let Some(messages) = data.get("thread_messages_list") {
- let messages: Vec = serde_json::from_value(messages.clone())
- .map_err(|e| format!("Failed to parse thread messages: {}", e))?;
- return Ok(messages);
- }
- }
- Err(format!("Unexpected response format: {}", response_body))
- } else {
- let status = response.status();
- let error_text = response
- .text()
- .await
- .unwrap_or_else(|_| "Unknown error".to_string());
- Err(format!(
- "Failed to get thread messages: HTTP status {}, error: {}",
- status, error_text
- ))
- }
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ user_agent: Some("refact-lsp".to_string()),
+ additional_headers: None,
+ };
+
+ tracing::info!("get_thread_messages: address={}, thread_id={}, alt={}",
+ config.address, thread_id, alt
+ );
+ execute_graphql::, _>(
+ config,
+ query,
+ variables,
+ "thread_messages_list"
+ )
+ .await
+ .map_err(graphql_error_to_string)
}
pub async fn create_thread_messages(
- gcx: Arc>,
+ cmd_address_url: &str,
+ api_key: &str,
thread_id: &str,
messages: Vec,
) -> Result<(), String> {
if messages.is_empty() {
return Err("No messages provided".to_string());
}
- let client = Client::new();
- let api_key = gcx.read().await.cmdline.api_key.clone();
let mut input_messages = Vec::with_capacity(messages.len());
+ let messages_len = messages.len();
for message in messages {
if message.ftm_belongs_to_ft_id != thread_id {
return Err(format!(
@@ -142,7 +115,8 @@ pub async fn create_thread_messages(
"ftm_tool_calls": tool_calls_str,
"ftm_call_id": message.ftm_call_id,
"ftm_usage": usage_str,
- "ftm_provenance": serde_json::to_string(&message.ftm_provenance).unwrap()
+ "ftm_provenance": serde_json::to_string(&message.ftm_provenance).unwrap(),
+ "ftm_user_preferences": serde_json::to_string(&message.ftm_user_preferences).unwrap()
}));
}
let variables = json!({
@@ -153,59 +127,27 @@ pub async fn create_thread_messages(
});
let mutation = r#"
mutation ThreadMessagesCreateMultiple($input: FThreadMultipleMessagesInput!) {
- thread_messages_create_multiple(input: $input) {
- count
- messages {
- ftm_belongs_to_ft_id
- ftm_alt
- ftm_num
- ftm_prev_alt
- ftm_role
- ftm_created_ts
- ftm_call_id
- ftm_provenance
- }
- }
+ thread_messages_create_multiple(input: $input)
}
"#;
- let response = client
- .post(&crate::constants::GRAPHQL_URL.to_string())
- .header("Authorization", format!("Bearer {}", api_key))
- .header("Content-Type", "application/json")
- .json(&json!({
- "query": mutation,
- "variables": variables
- }))
- .send()
- .await
- .map_err(|e| format!("Failed to send GraphQL request: {}", e))?;
- if response.status().is_success() {
- let response_body = response
- .text()
- .await
- .map_err(|e| format!("Failed to read response body: {}", e))?;
- let response_json: Value = serde_json::from_str(&response_body)
- .map_err(|e| format!("Failed to parse response JSON: {}", e))?;
- if let Some(errors) = response_json.get("errors") {
- let error_msg = errors.to_string();
- error!("GraphQL error: {}", error_msg);
- return Err(format!("GraphQL error: {}", error_msg));
- }
- if let Some(_) = response_json.get("data") {
- return Ok(())
- }
- Err(format!("Unexpected response format: {}", response_body))
- } else {
- let status = response.status();
- let error_text = response
- .text()
- .await
- .unwrap_or_else(|_| "Unknown error".to_string());
- Err(format!(
- "Failed to create thread messages: HTTP status {}, error: {}",
- status, error_text
- ))
- }
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ user_agent: Some("refact-lsp".to_string()),
+ additional_headers: None,
+ };
+ tracing::info!("create_thread_messages: address={}, thread_id={}, messages_len={}",
+ config.address, thread_id, messages_len
+ );
+ execute_graphql_no_result(
+ config,
+ mutation,
+ variables,
+ "thread_messages_create_multiple"
+ )
+ .await
+ .map_err(graphql_error_to_string)
}
pub fn convert_thread_messages_to_messages(
@@ -228,9 +170,6 @@ pub fn convert_thread_messages_to_messages(
tool_calls,
tool_call_id: msg.ftm_call_id.clone(),
tool_failed: None,
- usage: msg.ftm_usage.clone().map(|u| {
- serde_json::from_value::(u).unwrap_or_else(|_| ChatUsage::default())
- }),
checkpoints: vec![],
thinking_blocks: None,
finish_reason: None,
@@ -241,10 +180,11 @@ pub fn convert_thread_messages_to_messages(
pub fn convert_messages_to_thread_messages(
messages: Vec,
- alt: i32,
- prev_alt: i32,
- start_num: i32,
+ alt: i64,
+ prev_alt: i64,
+ start_num: i64,
thread_id: &str,
+ user_preferences: Option,
) -> Result, String> {
let mut output_messages = Vec::new();
let flush_delayed_images = |results: &mut Vec, delay_images: &mut Vec| {
@@ -252,7 +192,7 @@ pub fn convert_messages_to_thread_messages(
delay_images.clear();
};
for (i, msg) in messages.into_iter().enumerate() {
- let num = start_num + i as i32;
+ let num = start_num + i as i64;
let mut delay_images = vec![];
let mut messages = if msg.role == "tool" {
let mut results = vec![];
@@ -341,6 +281,7 @@ pub fn convert_messages_to_thread_messages(
.unwrap_or_default()
.as_secs_f64(),
ftm_provenance: json!({"system_type": "refact_lsp", "version": env!("CARGO_PKG_VERSION") }),
+ ftm_user_preferences: user_preferences.clone(),
})
}
}
diff --git a/refact-agent/engine/src/cloud/mod.rs b/refact-agent/engine/src/cloud/mod.rs
index e24f2d519..727d668e2 100644
--- a/refact-agent/engine/src/cloud/mod.rs
+++ b/refact-agent/engine/src/cloud/mod.rs
@@ -2,3 +2,8 @@ pub mod threads_sub;
mod threads_req;
mod messages_req;
mod experts_req;
+pub mod subchat;
+mod threads_processing;
+mod cloud_tools_req;
+pub mod graphql_client;
+pub mod memories_req;
diff --git a/refact-agent/engine/src/cloud/subchat.rs b/refact-agent/engine/src/cloud/subchat.rs
new file mode 100644
index 000000000..9981f35f7
--- /dev/null
+++ b/refact-agent/engine/src/cloud/subchat.rs
@@ -0,0 +1,186 @@
+use std::sync::Arc;
+use std::sync::atomic::Ordering;
+use futures::StreamExt;
+use crate::at_commands::at_commands::AtCommandsContext;
+use tokio::sync::Mutex as AMutex;
+use crate::call_validation::{ChatMessage, ReasoningEffort};
+use crate::cloud::{threads_req, messages_req};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use tokio_tungstenite::tungstenite::Message;
+use tracing::{error, info};
+use crate::cloud::threads_sub::{initialize_connection, ThreadPayload};
+
+#[derive(Serialize, Deserialize, Debug)]
+struct KernelOutput {
+ pub logs: Vec,
+ pub detail: String,
+ pub flagged_by_kernel: bool
+}
+
+fn build_preferences(
+ model: &str,
+ temperature: Option,
+ max_new_tokens: Option,
+ n: usize,
+ reasoning_effort: Option,
+) -> serde_json::Value {
+ let mut preferences = serde_json::json!({
+ "model": model,
+ "n": n,
+ });
+ if let Some(temp) = temperature {
+ preferences["temperature"] = serde_json::json!(temp);
+ }
+ if let Some(max_tokens) = max_new_tokens {
+ preferences["max_new_tokens"] = serde_json::json!(max_tokens);
+ }
+ if let Some(reasoning_effort) = reasoning_effort {
+ preferences["reasoning_effort"] = serde_json::json!(reasoning_effort);
+ }
+ preferences
+}
+
+pub async fn subchat(
+ ccx: Arc>,
+ ft_fexp_id: &str,
+ tool_call_id: &str,
+ messages: Vec,
+ temperature: Option,
+ max_new_tokens: Option,
+ reasoning_effort: Option,
+) -> Result, String> {
+ let gcx = ccx.lock().await.global_context.clone();
+ let (cmd_address_url, api_key, app_searchable_id, located_fgroup_id, parent_thread_id) = {
+ let gcx_read = gcx.read().await;
+ let located_fgroup_id = gcx_read.active_group_id.clone()
+ .ok_or("No active group ID is set".to_string())?;
+ (
+ gcx_read.cmdline.address_url.clone(),
+ gcx_read.cmdline.api_key.clone(),
+ gcx_read.app_searchable_id.clone(),
+ located_fgroup_id,
+ ccx.lock().await.chat_id.clone(),
+ )
+ };
+
+ let model_name = crate::cloud::experts_req::expert_choice_consequences(&cmd_address_url, &api_key, ft_fexp_id, &located_fgroup_id).await?;
+ let preferences = build_preferences(&model_name, temperature, max_new_tokens, 1, reasoning_effort);
+ let existing_threads = crate::cloud::threads_req::get_threads_app_captured(
+ &cmd_address_url,
+ &api_key,
+ &located_fgroup_id,
+ &app_searchable_id,
+ tool_call_id
+ ).await?;
+ let thread = if !existing_threads.is_empty() {
+ info!("There are already existing threads for this tool_id: {:?}", existing_threads);
+ existing_threads[0].clone()
+ } else {
+ let thread = threads_req::create_thread(
+ &cmd_address_url,
+ &api_key,
+ &located_fgroup_id,
+ ft_fexp_id,
+ &format!("subchat_{}", ft_fexp_id),
+ &tool_call_id,
+ &app_searchable_id,
+ serde_json::json!({
+ "tool_call_id": tool_call_id,
+ "ft_fexp_id": ft_fexp_id,
+ }),
+ None,
+ Some(parent_thread_id)
+ ).await?;
+ let thread_messages = messages_req::convert_messages_to_thread_messages(
+ messages, 100, 100, 1, &thread.ft_id, Some(preferences)
+ )?;
+ messages_req::create_thread_messages(
+ &cmd_address_url, &api_key, &thread.ft_id, thread_messages
+ ).await?;
+ thread
+ };
+
+ let thread_id = thread.ft_id.clone();
+ let connection_result = initialize_connection(&cmd_address_url, &api_key, &located_fgroup_id).await;
+ let mut connection = match connection_result {
+ Ok(conn) => conn,
+ Err(err) => return Err(format!("Failed to initialize WebSocket connection: {}", err)),
+ };
+ while let Some(msg) = connection.next().await {
+ if gcx.read().await.shutdown_flag.load(Ordering::SeqCst) {
+ info!("shutting down threads subscription");
+ break;
+ }
+ match msg {
+ Ok(Message::Text(text)) => {
+ let response: Value = match serde_json::from_str(&text) {
+ Ok(res) => res,
+ Err(err) => {
+ error!("failed to parse message: {}, error: {}", text, err);
+ continue;
+ }
+ };
+ match response["type"].as_str().unwrap_or("unknown") {
+ "data" => {
+ if let Some(payload) = response["payload"].as_object() {
+ let data = &payload["data"];
+ let threads_in_group = &data["threads_in_group"];
+ let news_action = threads_in_group["news_action"].as_str().unwrap_or("");
+ if news_action != "INSERT" && news_action != "UPDATE" {
+ continue;
+ }
+ if let Ok(payload) = serde_json::from_value::(threads_in_group["news_payload"].clone()) {
+ if payload.ft_id != thread_id {
+ continue;
+ }
+ if payload.ft_error.is_some() {
+ break;
+ }
+ } else {
+ info!("failed to parse thread payload: {:?}", threads_in_group);
+ }
+ } else {
+ info!("received data message but couldn't find payload");
+ }
+ }
+ "error" => {
+ error!("threads subscription error: {}", text);
+ }
+ "complete" => {
+ error!("threads subscription complete: {}.\nRestarting it", text);
+ }
+ _ => {
+ info!("received message with unknown type: {}", text);
+ }
+ }
+ }
+ Ok(Message::Close(_)) => {
+ info!("webSocket connection closed");
+ break;
+ }
+ Ok(_) => {}
+ Err(e) => {
+ return Err(format!("webSocket error: {}", e));
+ }
+ }
+ }
+
+ let thread = threads_req::get_thread(&cmd_address_url, &api_key, &thread_id).await?;
+ if let Some(error) = thread.ft_error {
+ // the error might be actually a kernel output data
+ if let Some(kernel_output) = serde_json::from_str::(&error.to_string()).ok() {
+ info!("subchat was terminated by kernel: {:?}", kernel_output);
+ } else {
+ return Err(format!("Thread error: {:?}", error));
+ }
+ }
+
+ let all_thread_messages = messages_req::get_thread_messages(
+ &cmd_address_url, &api_key, &thread_id, 100
+ ).await?;
+ Ok(messages_req::convert_thread_messages_to_messages(&all_thread_messages)
+ .into_iter()
+ .filter(|x| x.role != "kernel")
+ .collect::>())
+}
diff --git a/refact-agent/engine/src/cloud/threads_processing.rs b/refact-agent/engine/src/cloud/threads_processing.rs
new file mode 100644
index 000000000..c85087cff
--- /dev/null
+++ b/refact-agent/engine/src/cloud/threads_processing.rs
@@ -0,0 +1,448 @@
+use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
+use indexmap::IndexMap;
+use tokio::sync::RwLock as ARwLock;
+use tokio::sync::Mutex as AMutex;
+use serde_json::{json, Value};
+use tracing::{error, info, warn};
+use crate::at_commands::at_commands::AtCommandsContext;
+use crate::basic_utils::generate_random_hash;
+use crate::call_validation::{ChatContent, ChatMessage, ChatToolCall, ContextEnum, ContextFile};
+use crate::cloud::messages_req::ThreadMessage;
+use crate::cloud::threads_req::{lock_thread, Thread};
+use crate::cloud::threads_sub::{BasicStuff, ThreadPayload};
+use crate::global_context::GlobalContext;
+use crate::scratchpads::scratchpad_utils::max_tokens_for_rag_chat_by_tools;
+use crate::tools::tools_description::{MatchConfirmDeny, MatchConfirmDenyResult, Tool};
+use crate::tools::tools_execute::pp_run_tools;
+
+
+pub async fn match_against_confirm_deny(
+ ccx: Arc>,
+ t_call: &ChatToolCall,
+ tool: &mut Box,
+) -> Result {
+ let args = match serde_json::from_str::>(&t_call.function.arguments) {
+ Ok(args) => args,
+ Err(e) => {
+ return Err(format!("Tool use: couldn't parse arguments: {}. Error:\n{}", t_call.function.arguments, e));
+ }
+ };
+ Ok(tool.match_against_confirm_deny(ccx.clone(), &args).await?)
+}
+
+pub async fn run_tool(
+ ccx: Arc>,
+ t_call: &ChatToolCall,
+ tool: &mut Box,
+) -> Result<(ChatMessage, Vec, Vec), String> {
+ let args = match serde_json::from_str::>(&t_call.function.arguments) {
+ Ok(args) => args,
+ Err(e) => {
+ return Err(format!("Tool use: couldn't parse arguments: {}. Error:\n{}", t_call.function.arguments, e));
+ }
+ };
+
+ match tool.match_against_confirm_deny(ccx.clone(), &args).await {
+ Ok(res) => {
+ match res.result {
+ MatchConfirmDenyResult::DENY => {
+ let command_to_match = tool
+ .command_to_match_against_confirm_deny(ccx.clone(), &args).await
+ .unwrap_or("".to_string());
+ return Err(format!("tool use: command '{command_to_match}' is denied"));
+ }
+ _ => {}
+ }
+ }
+ Err(err) => return Err(err),
+ };
+
+ let tool_execute_results = match tool.tool_execute(ccx.clone(), &t_call.id.to_string(), &args).await {
+ Ok((_, mut tool_execute_results)) => {
+ for tool_execute_result in &mut tool_execute_results {
+ if let ContextEnum::ChatMessage(m) = tool_execute_result {
+ m.tool_failed = Some(false);
+ }
+ }
+ tool_execute_results
+ }
+ Err(e) => {
+ return Err(e);
+ }
+ };
+
+ let (mut tool_result_mb, mut other_messages, mut context_files) = (None, vec![], vec![]);
+ for msg in tool_execute_results {
+ match msg {
+ ContextEnum::ChatMessage(m) => {
+ if !m.tool_call_id.is_empty() {
+ if tool_result_mb.is_some() {
+ return Err(format!("duplicated output message from the tool: {}", t_call.function.name));
+ }
+ tool_result_mb = Some(m);
+ } else {
+ other_messages.push(m);
+ }
+ },
+ ContextEnum::ContextFile(m) => {
+ context_files.push(m);
+ }
+ }
+ }
+ let tool_result = match tool_result_mb {
+ Some(m) => m,
+ None => return Err(format!("tool use: failed to get output message from tool: {}", t_call.function.name)),
+ };
+
+ Ok((tool_result, other_messages, context_files))
+}
+
+async fn initialize_thread(
+ gcx: Arc>,
+ ft_fexp_id: &str,
+ thread: &Thread,
+ cmd_address_url: &str,
+ api_key: &str,
+ located_fgroup_id: &str
+) -> Result<(), String> {
+ let expert = crate::cloud::experts_req::get_expert(cmd_address_url, api_key, ft_fexp_id).await?;
+ let cloud_tools = crate::cloud::cloud_tools_req::get_cloud_tools(cmd_address_url, api_key, located_fgroup_id).await?;
+ let tools: Vec> =
+ crate::tools::tools_list::get_available_tools(gcx.clone())
+ .await
+ .into_iter()
+ .filter(|tool| expert.is_tool_allowed(&tool.tool_description().name))
+ .collect();
+ let tool_names = tools.iter().map(|x| x.tool_description().name.clone()).collect::>();
+ let mut tool_descriptions: Vec<_> = tools
+ .iter()
+ .map(|x| x.tool_description().into_openai_style())
+ .collect();
+ tool_descriptions.extend(
+ cloud_tools.into_iter()
+ .filter(|x| expert.is_tool_allowed(&x.ctool_name))
+ .filter(|x| {
+ if tool_names.contains(&x.ctool_name) {
+ error!("tool `{}` is already in the toolset, filtering it out. This might cause races between cloud and binary", x.ctool_name);
+ false
+ } else { true }
+ })
+ .map(|x| x.into_openai_style())
+ );
+ crate::cloud::threads_req::set_thread_toolset(cmd_address_url, api_key, &thread.ft_id, tool_descriptions).await?;
+ let updated_system_prompt = crate::scratchpads::chat_utils_prompts::system_prompt_add_extra_instructions(
+ gcx.clone(), expert.fexp_system_prompt.clone(), HashSet::new()
+ ).await;
+ let output_thread_messages = vec![ThreadMessage {
+ ftm_belongs_to_ft_id: thread.ft_id.clone(),
+ ftm_alt: 100, // convention, system prompt always at num=0, alt=100
+ ftm_num: 0,
+ ftm_prev_alt: 100,
+ ftm_role: "system".to_string(),
+ ftm_content: Some(
+ serde_json::to_value(ChatContent::SimpleText(updated_system_prompt)).unwrap(),
+ ),
+ ftm_tool_calls: None,
+ ftm_call_id: "".to_string(),
+ ftm_usage: None,
+ ftm_created_ts: std::time::SystemTime::now() // XXX not accepted by server
+ .duration_since(std::time::SystemTime::UNIX_EPOCH)
+ .unwrap()
+ .as_secs_f64(),
+ ftm_provenance: json!({"system_type": "refact_lsp", "version": env!("CARGO_PKG_VERSION") }),
+ ftm_user_preferences: None,
+ }];
+ crate::cloud::messages_req::create_thread_messages(
+ &cmd_address_url,
+ &api_key,
+ &thread.ft_id,
+ output_thread_messages,
+ ).await?;
+ Ok(())
+}
+
+async fn call_tools(
+ gcx: Arc>,
+ thread: &Thread,
+ thread_messages: &Vec,
+ alt: i64,
+ cmd_address_url: &str,
+ api_key: &str
+) -> Result<(), String> {
+ // TODO: think of better ways to handle these params
+ let n_ctx = 128000;
+ let top_n = 12;
+ let max_new_tokens = 8192;
+
+ let last_message = thread_messages.iter()
+ .max_by_key(|x| x.ftm_num)
+ .ok_or("No last message found".to_string())
+ .clone()?;
+ let last_tool_calls = thread_messages.iter()
+ .rev()
+ .find(|x| x.ftm_role == "assistant" && x.ftm_tool_calls.is_some())
+ .cloned()
+ .map(|x|
+ crate::cloud::messages_req::convert_thread_messages_to_messages(&vec![x.clone()])[0].clone()
+ )
+ .map(|x| x.tool_calls.clone().expect("checked before"))
+ .ok_or("No last assistant message with tool calls found".to_string())?;
+ let messages = crate::cloud::messages_req::convert_thread_messages_to_messages(thread_messages)
+ .into_iter()
+ .filter(|x| x.role != "kernel")
+ .collect::>();
+ let ccx = Arc::new(AMutex::new(
+ AtCommandsContext::new(gcx.clone(), n_ctx, top_n, false, messages.clone(),
+ thread.ft_id.to_string(), false
+ ).await,
+ ));
+ let toolset = thread.ft_toolset.clone().unwrap_or_default();
+ let allowed_tools = crate::cloud::messages_req::get_tool_names_from_openai_format(&toolset).await?;
+ let mut all_tools: IndexMap> =
+ crate::tools::tools_list::get_available_tools(gcx.clone()).await
+ .into_iter()
+ .filter(|x| allowed_tools.contains(&x.tool_description().name))
+ .map(|x| (x.tool_description().name, x))
+ .collect();
+ let mut all_tool_output_messages = vec![];
+ let mut all_context_files = vec![];
+ let mut all_other_messages = vec![];
+ let mut tool_id_to_num = HashMap::new();
+ let mut required_confirmation = vec![];
+ // Default tokens limit for tools that perform internal compression (`tree()`, ...)
+ ccx.lock().await.tokens_for_rag = max_new_tokens;
+
+ let confirmed_tool_call_ids: Vec = if let Some(confirmation_response) = &thread.ft_confirmation_response {
+ if confirmation_response.as_str().unwrap_or("") == "*" {
+ last_tool_calls.iter().map(|x| x.id.clone()).collect()
+ } else {
+ serde_json::from_value(confirmation_response.clone()).map_err(|err| {
+ format!("error parsing confirmation response: {}", err)
+ })?
+ }
+ } else { vec![] };
+ let waiting_for_confirmation = if let Some(confirmation_response) = &thread.ft_confirmation_response {
+ match serde_json::from_value::>(confirmation_response.clone()) {
+ Ok(items) => {
+ items.iter()
+ .filter_map(|item| item.get("tool_call_id").and_then(|id| id.as_str()))
+ .map(|s| s.to_string())
+ .collect::>()
+ }
+ Err(err) => {
+ warn!("error parsing confirmation response: {}", err);
+ vec![]
+ }
+ }
+ } else { vec![] };
+ for (idx, t_call) in last_tool_calls.iter().enumerate() {
+ let is_answered = thread_messages.iter()
+ .filter(|x| x.ftm_role == "tool")
+ .any(|x| t_call.id == x.ftm_call_id);
+ if is_answered {
+ warn!("tool use: tool call `{}` is already answered, skipping it", t_call.id);
+ continue;
+ }
+
+ let tool = match all_tools.get_mut(&t_call.function.name) {
+ Some(tool) => tool,
+ None => {
+ warn!("tool use: function {:?} not found", &t_call.function.name);
+ continue;
+ }
+ };
+ let skip_confirmation = confirmed_tool_call_ids.contains(&t_call.id);
+ if !skip_confirmation {
+ let confirm_deny_res = match_against_confirm_deny(ccx.clone(), t_call, tool).await?;
+ match &confirm_deny_res.result {
+ MatchConfirmDenyResult::CONFIRMATION => {
+ info!("tool use: tool call `{}` requires confirmation, skipping it", t_call.id);
+ if !waiting_for_confirmation.contains(&t_call.id) {
+ required_confirmation.push(json!({
+ "tool_call_id": t_call.id,
+ "command": confirm_deny_res.command,
+ "rule": confirm_deny_res.rule,
+ "ftm_num": last_message.ftm_num + 1 + idx as i64,
+ }));
+ }
+ continue;
+ }
+ _ => { }
+ }
+ } else {
+ info!("tool use: tool call `{}` is confirmed, processing to call it", t_call.id);
+ }
+
+ let (tool_result, other_messages, context_files) = match run_tool(ccx.clone(), t_call, tool).await {
+ Ok(res) => res,
+ Err(err) => {
+ warn!("tool use: failed to run tool: {}", err);
+ (
+ ChatMessage {
+ role: "tool".to_string(),
+ content: ChatContent::SimpleText(err),
+ tool_call_id: t_call.id.clone(),
+ ..ChatMessage::default()
+ }, vec![], vec![]
+ )
+ }
+ };
+ all_tool_output_messages.push(tool_result);
+ all_context_files.extend(context_files);
+ all_other_messages.extend(other_messages);
+ tool_id_to_num.insert(t_call.id.clone(), last_message.ftm_num + 1 + idx as i64);
+ }
+
+ let reserve_for_context = max_tokens_for_rag_chat_by_tools(&last_tool_calls, &all_context_files, n_ctx, max_new_tokens);
+ ccx.lock().await.tokens_for_rag = reserve_for_context;
+ let (generated_tool, generated_other) = pp_run_tools(
+ ccx.clone(), &vec![], false, all_tool_output_messages,
+ all_other_messages, &mut all_context_files, reserve_for_context, &None,
+ ).await;
+ let mut afterwards_num = last_message.ftm_num + last_tool_calls.len() as i64 + 1;
+ let mut all_output_messages = vec![];
+ for msg in generated_tool.into_iter().chain(generated_other.into_iter()) {
+ let dest_num = if let Some(dest_num) = tool_id_to_num.get(&msg.tool_call_id) {
+ dest_num.clone()
+ } else {
+ afterwards_num += 1;
+ afterwards_num - 1
+ };
+ let output_thread_messages = crate::cloud::messages_req::convert_messages_to_thread_messages(
+ vec![msg], alt, alt, dest_num, &thread.ft_id, last_message.ftm_user_preferences.clone(),
+ )?;
+ all_output_messages.extend(output_thread_messages);
+ }
+
+ if !required_confirmation.is_empty() {
+ if !crate::cloud::threads_req::set_thread_confirmation_request(
+ cmd_address_url, api_key, &thread.ft_id, serde_json::to_value(required_confirmation.clone()).unwrap()
+ ).await? {
+ warn!("tool use: cannot set confirmation requests: {:?}", required_confirmation);
+ }
+ }
+
+ if !all_output_messages.is_empty() {
+ crate::cloud::messages_req::create_thread_messages(cmd_address_url, api_key, &thread.ft_id, all_output_messages).await?;
+ } else {
+ info!("thread `{}` has no tool output messages. Skipping it", thread.ft_id);
+ }
+ Ok(())
+}
+
+pub async fn process_thread_event(
+ gcx: Arc>,
+ thread_payload: ThreadPayload,
+ basic_info: BasicStuff,
+ cmd_address_url: String,
+ api_key: String,
+ app_searchable_id: String,
+ located_fgroup_id: String,
+) -> Result<(), String> {
+ if thread_payload.ft_need_tool_calls == -1
+ || thread_payload.owner_fuser_id != basic_info.fuser_id
+ || !thread_payload.ft_locked_by.is_empty() {
+ return Ok(());
+ }
+ if let Some(ft_app_searchable) = thread_payload.ft_app_searchable.clone() {
+ if ft_app_searchable != app_searchable_id {
+ info!("thread `{}` has different `app_searchable` id, skipping it: {} != {}",
+ thread_payload.ft_id, app_searchable_id, ft_app_searchable
+ );
+ return Ok(());
+ }
+ } else {
+ info!("thread `{}` doesn't have the `app_searchable` id, skipping it", thread_payload.ft_id);
+ return Ok(());
+ }
+ if let Some(error) = thread_payload.ft_error.as_ref() {
+ info!("thread `{}` has the error: `{}`. Skipping it", thread_payload.ft_id, error);
+ return Ok(());
+ }
+
+ let hash = generate_random_hash(16);
+ let thread_id = thread_payload.ft_id.clone();
+ let lock_result = lock_thread(&cmd_address_url, &api_key, &thread_id, &hash).await;
+ if let Err(err) = lock_result {
+ info!("failed to lock thread `{}` with hash `{}`: {}", thread_id, hash, err);
+ return Ok(());
+ }
+ info!("thread `{}` locked successfully with hash `{}`", thread_id, hash);
+ let process_result = process_locked_thread(
+ gcx,
+ &thread_payload,
+ &thread_id,
+ &cmd_address_url,
+ &api_key,
+ &located_fgroup_id
+ ).await;
+ match crate::cloud::threads_req::unlock_thread(&cmd_address_url, &api_key, &thread_id, &hash).await {
+ Ok(_) => info!("thread `{}` unlocked successfully", thread_id),
+ Err(err) => {
+ error!("failed to unlock thread `{}`: {}", thread_id, err);
+ },
+ }
+ process_result
+}
+
+async fn process_locked_thread(
+ gcx: Arc>,
+ thread_payload: &ThreadPayload,
+ thread_id: &str,
+ cmd_address_url: &str,
+ api_key: &str,
+ located_fgroup_id: &str
+) -> Result<(), String> {
+ let alt = thread_payload.ft_need_tool_calls;
+ let messages = match crate::cloud::messages_req::get_thread_messages(
+ &cmd_address_url,
+ &api_key,
+ thread_id,
+ thread_payload.ft_need_tool_calls,
+ ).await {
+ Ok(msgs) => msgs,
+ Err(e) => {
+ return Err(e);
+ }
+ };
+ if messages.is_empty() {
+ info!("thread `{}` has no messages. Skipping it", thread_id);
+ return Ok(());
+ }
+ let thread = match crate::cloud::threads_req::get_thread(cmd_address_url, api_key, thread_id).await {
+ Ok(t) => t,
+ Err(e) => {
+ return Err(e);
+ }
+ };
+ let need_to_append_system = messages.iter().all(|x| x.ftm_role != "system");
+ if need_to_append_system {
+ if thread_payload.ft_fexp_id.is_none() {
+ info!("thread `{}` has no expert set. Skipping it", thread_id);
+ return Ok(());
+ }
+ } else {
+ if thread.ft_toolset.is_none() {
+ info!("thread `{}` has no toolset. Skipping it", thread_id);
+ return Ok(());
+ }
+ }
+ let result = if need_to_append_system {
+ let ft_fexp_id = thread.ft_fexp_id.clone().expect("checked before");
+ info!("initializing system prompt for thread `{}`", thread_id);
+ initialize_thread(gcx.clone(), &ft_fexp_id, &thread, cmd_address_url, api_key, located_fgroup_id).await
+ } else {
+ info!("calling tools for thread `{}`", thread_id);
+ call_tools(gcx.clone(), &thread, &messages, alt, cmd_address_url, api_key).await
+ };
+ if let Err(err) = &result {
+ info!("failed to process thread `{}`, setting error: {}", thread_id, err);
+ if let Err(set_err) = crate::cloud::threads_req::set_error_thread(
+ cmd_address_url, api_key, thread_id, err
+ ).await {
+ return Err(format!("Failed to set error on thread: {}", set_err));
+ }
+ }
+ result
+}
diff --git a/refact-agent/engine/src/cloud/threads_req.rs b/refact-agent/engine/src/cloud/threads_req.rs
index fbd74d4b9..efa7b9e02 100644
--- a/refact-agent/engine/src/cloud/threads_req.rs
+++ b/refact-agent/engine/src/cloud/threads_req.rs
@@ -1,42 +1,110 @@
-use log::error;
-use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
-use std::sync::Arc;
-use tokio::sync::RwLock as ARwLock;
-use crate::global_context::GlobalContext;
-
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Thread {
pub owner_fuser_id: String,
pub owner_shared: bool,
pub located_fgroup_id: String,
pub ft_id: String,
- pub ft_fexp_name: String,
+ pub ft_fexp_id: Option,
pub ft_title: String,
- pub ft_toolset: Vec,
- pub ft_belongs_to_fce_id: Option,
- pub ft_model: String,
- pub ft_temperature: f64,
- pub ft_max_new_tokens: i32,
- pub ft_n: i32,
- pub ft_error: Option,
- pub ft_need_assistant: i32,
- pub ft_need_tool_calls: i32,
- pub ft_anything_new: bool,
+ pub ft_toolset: Option>,
+ pub ft_error: Option,
+ pub ft_need_assistant: i64,
+ pub ft_need_tool_calls: i64,
+ pub ft_need_user: i64,
pub ft_created_ts: f64,
pub ft_updated_ts: f64,
pub ft_archived_ts: f64,
pub ft_locked_by: String,
+ pub ft_confirmation_request: Option,
+ pub ft_confirmation_response: Option,
+}
+
+pub async fn create_thread(
+ cmd_address_url: &str,
+ api_key: &str,
+ located_fgroup_id: &str,
+ ft_fexp_id: &str,
+ ft_title: &str,
+ ft_app_capture: &str,
+ ft_app_searchable: &str,
+ ft_app_specific: Value,
+ ft_toolset: Option>,
+ parent_ft_id: Option,
+) -> Result {
+ use crate::cloud::graphql_client::{execute_graphql, GraphQLRequestConfig};
+
+ let mutation = r#"
+ mutation CreateThread($input: FThreadInput!) {
+ thread_create(input: $input) {
+ owner_fuser_id
+ owner_shared
+ located_fgroup_id
+ ft_id
+ ft_fexp_id
+ ft_title
+ ft_error
+ ft_toolset
+ ft_need_assistant
+ ft_need_tool_calls
+ ft_need_user
+ ft_created_ts
+ ft_updated_ts
+ ft_archived_ts
+ ft_locked_by
+ ft_confirmation_request
+ ft_confirmation_response
+ }
+ }
+ "#;
+
+ let toolset_str = match ft_toolset {
+ Some(toolset) => serde_json::to_string(&toolset).map_err(|e| format!("Failed to serialize toolset: {}", e))?,
+ None => "null".to_string(),
+ };
+
+ let mut input = json!({
+ "owner_shared": false,
+ "located_fgroup_id": located_fgroup_id,
+ "ft_fexp_id": ft_fexp_id,
+ "ft_title": ft_title,
+ "ft_toolset": toolset_str,
+ "ft_app_capture": ft_app_capture,
+ "ft_app_searchable": ft_app_searchable,
+ "ft_app_specific": serde_json::to_string(&ft_app_specific).unwrap(),
+ });
+
+ if let Some(parent_id) = parent_ft_id {
+ input["parent_ft_id"] = json!(parent_id);
+ }
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
+ tracing::info!("create_thread: address={}, ft_title={}, ft_app_capture={}, ft_app_searchable={}",
+ config.address, ft_title, ft_app_capture, ft_app_searchable
+ );
+ execute_graphql::(
+ config,
+ mutation,
+ json!({"input": input}),
+ "thread_create"
+ )
+ .await
+ .map_err(|e| e.to_string())
}
pub async fn get_thread(
- gcx: Arc>,
+ cmd_address_url: &str,
+ api_key: &str,
thread_id: &str,
) -> Result {
- let client = Client::new();
- let api_key = gcx.read().await.cmdline.api_key.clone();
+ use crate::cloud::graphql_client::{execute_graphql, GraphQLRequestConfig};
+
let query = r#"
query GetThread($id: String!) {
thread_get(id: $id) {
@@ -44,80 +112,104 @@ pub async fn get_thread(
owner_shared
located_fgroup_id
ft_id
- ft_fexp_name,
+ ft_fexp_id,
ft_title
- ft_belongs_to_fce_id
- ft_model
- ft_temperature
- ft_max_new_tokens
- ft_n
ft_error
ft_toolset
ft_need_assistant
ft_need_tool_calls
- ft_anything_new
+ ft_need_user
ft_created_ts
ft_updated_ts
ft_archived_ts
ft_locked_by
+ ft_confirmation_request
+ ft_confirmation_response
}
}
"#;
- let response = client
- .post(&crate::constants::GRAPHQL_URL.to_string())
- .header("Authorization", format!("Bearer {}", api_key))
- .header("Content-Type", "application/json")
- .json(&json!({
- "query": query,
- "variables": {"id": thread_id}
- }))
- .send()
- .await
- .map_err(|e| format!("Failed to send GraphQL request: {}", e))?;
- if response.status().is_success() {
- let response_body = response
- .text()
- .await
- .map_err(|e| format!("Failed to read response body: {}", e))?;
- let response_json: Value = serde_json::from_str(&response_body)
- .map_err(|e| format!("Failed to parse response JSON: {}", e))?;
- if let Some(errors) = response_json.get("errors") {
- let error_msg = errors.to_string();
- error!("GraphQL error: {}", error_msg);
- return Err(format!("GraphQL error: {}", error_msg));
- }
- if let Some(data) = response_json.get("data") {
- if let Some(thread_value) = data.get("thread_get") {
- let thread: Thread = serde_json::from_value(thread_value.clone())
- .map_err(|e| format!("Failed to parse thread: {}", e))?;
- return Ok(thread);
- }
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
+ tracing::info!("get_thread: address={}, thread_id={}", config.address, thread_id);
+ execute_graphql::(
+ config,
+ query,
+ json!({"id": thread_id}),
+ "thread_get"
+ )
+ .await
+ .map_err(|e| e.to_string())
+}
+
+pub async fn get_threads_app_captured(
+ cmd_address_url: &str,
+ api_key: &str,
+ located_fgroup_id: &str,
+ ft_app_searchable: &str,
+ ft_app_capture: &str,
+) -> Result, String> {
+ use crate::cloud::graphql_client::{execute_graphql, GraphQLRequestConfig};
+
+ let query = r#"
+ query GetThread($located_fgroup_id: String!, $ft_app_capture: String!, $ft_app_searchable: String!) {
+ threads_app_captured(located_fgroup_id: $located_fgroup_id, ft_app_capture: $ft_app_capture, ft_app_searchable: $ft_app_searchable) {
+ owner_fuser_id
+ owner_shared
+ located_fgroup_id
+ ft_id
+ ft_fexp_id,
+ ft_title
+ ft_error
+ ft_toolset
+ ft_need_assistant
+ ft_need_tool_calls
+ ft_need_user
+ ft_created_ts
+ ft_updated_ts
+ ft_archived_ts
+ ft_locked_by
+ ft_confirmation_request
+ ft_confirmation_response
}
- Err(format!(
- "Thread not found or unexpected response format: {}",
- response_body
- ))
- } else {
- let status = response.status();
- let error_text = response
- .text()
- .await
- .unwrap_or_else(|_| "Unknown error".to_string());
- Err(format!(
- "Failed to get thread: HTTP status {}, error: {}",
- status, error_text
- ))
}
+ "#;
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
+
+ let variables = json!({
+ "located_fgroup_id": located_fgroup_id,
+ "ft_app_capture": ft_app_capture,
+ "ft_app_searchable": ft_app_searchable
+ });
+ tracing::info!("get_threads_app_captured: address={}, located_fgroup_id={}, ft_app_capture={}, ft_app_searchable={}",
+ config.address, located_fgroup_id, ft_app_capture, ft_app_searchable
+ );
+ execute_graphql::, _>(
+ config,
+ query,
+ variables,
+ "threads_app_captured"
+ )
+ .await
+ .map_err(|e| e.to_string())
}
pub async fn set_thread_toolset(
- gcx: Arc>,
+ cmd_address_url: &str,
+ api_key: &str,
thread_id: &str,
- ft_toolset: Vec,
+ ft_toolset: Vec
) -> Result, String> {
- let client = Client::new();
- let api_key = gcx.read().await.cmdline.api_key.clone();
+ use crate::cloud::graphql_client::{execute_graphql, GraphQLRequestConfig};
+
let mutation = r#"
mutation UpdateThread($thread_id: String!, $patch: FThreadPatch!) {
thread_patch(id: $thread_id, patch: $patch) {
@@ -125,176 +217,208 @@ pub async fn set_thread_toolset(
}
}
"#;
+
let variables = json!({
"thread_id": thread_id,
"patch": {
"ft_toolset": serde_json::to_string(&ft_toolset).unwrap()
}
});
- let response = client
- .post(&crate::constants::GRAPHQL_URL.to_string())
- .header("Authorization", format!("Bearer {}", api_key))
- .header("Content-Type", "application/json")
- .json(&json!({
- "query": mutation,
- "variables": variables
- }))
- .send()
- .await
- .map_err(|e| format!("Failed to send GraphQL request: {}", e))?;
- if response.status().is_success() {
- let response_body = response
- .text()
- .await
- .map_err(|e| format!("Failed to read response body: {}", e))?;
- let response_json: Value = serde_json::from_str(&response_body)
- .map_err(|e| format!("Failed to parse response JSON: {}", e))?;
- if let Some(errors) = response_json.get("errors") {
- let error_msg = errors.to_string();
- error!("GraphQL error: {}", error_msg);
- return Err(format!("GraphQL error: {}", error_msg));
- }
- if let Some(data) = response_json.get("data") {
- if let Some(ft_toolset_json) = data.get("thread_patch") {
- let ft_toolset: Vec =
- serde_json::from_value(ft_toolset_json["ft_toolset"].clone())
- .map_err(|e| format!("Failed to parse updated thread: {}", e))?;
- return Ok(ft_toolset);
- }
- }
- Err(format!("Unexpected response format: {}", response_body))
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
+
+ tracing::info!("set_thread_toolset: address={}, thread_id={}, ft_toolset={:?}",
+ config.address, thread_id, ft_toolset
+ );
+ let result = execute_graphql::(
+ config,
+ mutation,
+ variables,
+ "thread_patch"
+ )
+ .await
+ .map_err(|e| e.to_string())?;
+ if let Some(ft_toolset_json) = result.get("ft_toolset") {
+ let ft_toolset: Vec = serde_json::from_value(ft_toolset_json.clone())
+ .map_err(|e| format!("Failed to parse updated thread: {}", e))?;
+ Ok(ft_toolset)
} else {
- let status = response.status();
- let error_text = response
- .text()
- .await
- .unwrap_or_else(|_| "Unknown error".to_string());
- Err(format!(
- "Failed to update thread: HTTP status {}, error: {}",
- status, error_text
- ))
+ Err("ft_toolset not found in response".to_string())
}
}
pub async fn lock_thread(
- gcx: Arc>,
+ cmd_address_url: &str,
+ api_key: &str,
thread_id: &str,
hash: &str,
) -> Result<(), String> {
- let client = Client::new();
- let api_key = gcx.read().await.cmdline.api_key.clone();
+ use crate::cloud::graphql_client::{execute_graphql_bool_result, GraphQLRequestConfig};
+
let worker_name = format!("refact-lsp:{hash}");
let query = r#"
mutation AdvanceLock($ft_id: String!, $worker_name: String!) {
thread_lock(ft_id: $ft_id, worker_name: $worker_name)
}
"#;
- let response = client
- .post(&crate::constants::GRAPHQL_URL.to_string())
- .header("Authorization", format!("Bearer {}", api_key))
- .header("Content-Type", "application/json")
- .json(&json!({
- "query": query,
- "variables": {"ft_id": thread_id, "worker_name": worker_name}
- }))
- .send()
- .await
- .map_err(|e| format!("Failed to send GraphQL request: {}", e))?;
-
- if response.status().is_success() {
- let response_body = response
- .text()
- .await
- .map_err(|e| format!("Failed to read response body: {}", e))?;
- let response_json: Value = serde_json::from_str(&response_body)
- .map_err(|e| format!("Failed to parse response JSON: {}", e))?;
- if let Some(errors) = response_json.get("errors") {
- let error_msg = errors.to_string();
- error!("GraphQL error: {}", error_msg);
- return Err(format!("GraphQL error: {}", error_msg));
- }
- if let Some(data) = response_json.get("data") {
- if data.get("thread_lock").is_some() {
- return Ok(());
- } else {
- return Err(format!("Thread {thread_id} is locked by another worker"));
- }
- }
- Err(format!(
- "Thread not found or unexpected response format: {}",
- response_body
- ))
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
+
+ let variables = json!({
+ "ft_id": thread_id,
+ "worker_name": worker_name
+ });
+
+ tracing::info!("lock_thread: address={}, thread_id={}, worker_name={}",
+ config.address, thread_id, worker_name
+ );
+ let result = execute_graphql_bool_result(
+ config,
+ query,
+ variables,
+ "thread_lock"
+ )
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if result {
+ Ok(())
} else {
- let status = response.status();
- let error_text = response
- .text()
- .await
- .unwrap_or_else(|_| "Unknown error".to_string());
- Err(format!(
- "Failed to get thread: HTTP status {}, error: {}",
- status, error_text
- ))
+ Err(format!("Thread {thread_id} is locked by another worker"))
}
}
pub async fn unlock_thread(
- gcx: Arc>,
- thread_id: String,
- hash: String,
+ cmd_address_url: &str,
+ api_key: &str ,
+ thread_id: &str,
+ hash: &str,
) -> Result<(), String> {
- let client = Client::new();
- let api_key = gcx.read().await.cmdline.api_key.clone();
+ use crate::cloud::graphql_client::{execute_graphql_bool_result, GraphQLRequestConfig};
+
let worker_name = format!("refact-lsp:{hash}");
let query = r#"
mutation AdvanceUnlock($ft_id: String!, $worker_name: String!) {
thread_unlock(ft_id: $ft_id, worker_name: $worker_name)
}
"#;
- let response = client
- .post(&crate::constants::GRAPHQL_URL.to_string())
- .header("Authorization", format!("Bearer {}", api_key))
- .header("Content-Type", "application/json")
- .json(&json!({
- "query": query,
- "variables": {"ft_id": thread_id, "worker_name": worker_name}
- }))
- .send()
- .await
- .map_err(|e| format!("Failed to send GraphQL request: {}", e))?;
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
+
+ let variables = json!({
+ "ft_id": thread_id,
+ "worker_name": worker_name
+ });
+
+ tracing::info!("unlock_thread: address={}, thread_id={}, worker_name={}",
+ config.address, thread_id, worker_name
+ );
+ let result = execute_graphql_bool_result(
+ config,
+ query,
+ variables,
+ "thread_unlock"
+ )
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if result {
+ Ok(())
+ } else {
+ Err(format!("Thread {thread_id} is locked by another worker"))
+ }
+}
+
+pub async fn set_error_thread(
+ cmd_address_url: &str,
+ api_key: &str,
+ thread_id: &str,
+ error: &str,
+) -> Result<(), String> {
+ use crate::cloud::graphql_client::{execute_graphql_no_result, GraphQLRequestConfig};
- if response.status().is_success() {
- let response_body = response
- .text()
- .await
- .map_err(|e| format!("Failed to read response body: {}", e))?;
- let response_json: Value = serde_json::from_str(&response_body)
- .map_err(|e| format!("Failed to parse response JSON: {}", e))?;
- if let Some(errors) = response_json.get("errors") {
- let error_msg = errors.to_string();
- error!("GraphQL error: {}", error_msg);
- return Err(format!("GraphQL error: {}", error_msg));
+ let mutation = r#"
+ mutation SetThreadError($thread_id: String!, $patch: FThreadPatch!) {
+ thread_patch(id: $thread_id, patch: $patch) {
+ ft_error
}
- if let Some(data) = response_json.get("data") {
- if data.get("thread_unlock").is_some() {
- return Ok(());
- } else {
- return Err(format!("Cannot unlock thread {thread_id}"));
- }
+ }
+ "#;
+
+ let variables = json!({
+ "thread_id": thread_id,
+ "patch": {
+ "ft_error": serde_json::to_string(&json!({"source": "refact_lsp", "error": error})).unwrap()
}
- Err(format!(
- "Thread not found or unexpected response format: {}",
- response_body
- ))
- } else {
- let status = response.status();
- let error_text = response
- .text()
- .await
- .unwrap_or_else(|_| "Unknown error".to_string());
- Err(format!(
- "Failed to get thread: HTTP status {}, error: {}",
- status, error_text
- ))
+ });
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
+
+ tracing::info!("unlock_thread: address={}, thread_id={}, ft_error={}",
+ config.address, thread_id, error
+ );
+ execute_graphql_no_result(
+ config,
+ mutation,
+ variables,
+ "thread_patch"
+ )
+ .await
+ .map_err(|e| e.to_string())
+}
+
+pub async fn set_thread_confirmation_request(
+ cmd_address_url: &str,
+ api_key: &str,
+ thread_id: &str,
+ confirmation_request: Value,
+) -> Result {
+ use crate::cloud::graphql_client::{execute_graphql_bool_result, GraphQLRequestConfig};
+
+ let mutation = r#"
+ mutation SetThreadConfirmationRequest($ft_id: String!, $confirmation_request: String!) {
+ thread_set_confirmation_request(ft_id: $ft_id, confirmation_request: $confirmation_request)
}
+ "#;
+
+ let confirmation_request_str = serde_json::to_string(&confirmation_request)
+ .map_err(|e| format!("Failed to serialize confirmation request: {}", e))?;
+
+ let variables = json!({
+ "ft_id": thread_id,
+ "confirmation_request": confirmation_request_str
+ });
+
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ ..Default::default()
+ };
+ tracing::info!("unlock_thread: address={}, thread_id={}, confirmation_request_str={:?}",
+ config.address, thread_id, confirmation_request_str
+ );
+ execute_graphql_bool_result(
+ config,
+ mutation,
+ variables,
+ "thread_set_confirmation_request"
+ )
+ .await
+ .map_err(|e| e.to_string())
}
diff --git a/refact-agent/engine/src/cloud/threads_sub.rs b/refact-agent/engine/src/cloud/threads_sub.rs
index e02d3941d..f9aed471d 100644
--- a/refact-agent/engine/src/cloud/threads_sub.rs
+++ b/refact-agent/engine/src/cloud/threads_sub.rs
@@ -1,27 +1,17 @@
-use std::collections::HashSet;
use crate::global_context::GlobalContext;
use futures::{SinkExt, StreamExt};
-use reqwest::Client;
+use crate::cloud::graphql_client::{execute_graphql, GraphQLRequestConfig, graphql_error_to_string};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
-use indexmap::IndexMap;
use tokio::sync::RwLock as ARwLock;
-use tokio::sync::Mutex as AMutex;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
use tracing::{error, info, warn};
use url::Url;
-use crate::at_commands::at_commands::AtCommandsContext;
-use crate::call_validation::ChatContent;
-use crate::cloud::messages_req::ThreadMessage;
-use crate::cloud::threads_req::{lock_thread, Thread};
-use rand::{Rng, thread_rng};
-use rand::distributions::Alphanumeric;
-use crate::custom_error::MapErrToString;
-
+use crate::basic_utils::generate_random_hash;
const RECONNECT_DELAY_SECONDS: u64 = 3;
@@ -29,18 +19,25 @@ const RECONNECT_DELAY_SECONDS: u64 = 3;
pub struct ThreadPayload {
pub owner_fuser_id: String,
pub ft_id: String,
- pub ft_error: Option,
+ pub ft_error: Option,
pub ft_locked_by: String,
+ pub ft_fexp_id: Option,
pub ft_need_tool_calls: i64,
+ pub ft_need_user: i64,
pub ft_app_searchable: Option,
+ pub ft_app_capture: Option,
+ pub ft_app_specific: Option,
+ pub ft_confirmation_request: Option,
+ pub ft_confirmation_response: Option,
}
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BasicStuff {
pub fuser_id: String,
pub workspaces: Vec,
}
+// XXX use xxx_subs::filter for ft_app_capture
const THREADS_SUBSCRIPTION_QUERY: &str = r#"
subscription ThreadsPageSubs($located_fgroup_id: String!) {
threads_in_group(located_fgroup_id: $located_fgroup_id) {
@@ -51,13 +48,20 @@ const THREADS_SUBSCRIPTION_QUERY: &str = r#"
ft_id
ft_error
ft_locked_by
+ ft_fexp_id
+ ft_confirmation_request
+ ft_confirmation_response
ft_need_tool_calls
+ ft_need_user
ft_app_searchable
+ ft_app_capture
+ ft_app_specific
}
}
}
"#;
+
pub async fn trigger_threads_subscription_restart(gcx: Arc>) {
let restart_flag = gcx.read().await.threads_subscription_restart_flag.clone();
restart_flag.store(true, Ordering::SeqCst);
@@ -65,10 +69,10 @@ pub async fn trigger_threads_subscription_restart(gcx: Arc>) {
- if !gcx.read().await.cmdline.cloud_threads {
- return;
- }
-
+ let (address_url, api_key) = {
+ let gcx_read = gcx.read().await;
+ (gcx_read.cmdline.address_url.clone(), gcx_read.cmdline.api_key.clone())
+ };
loop {
{
let restart_flag = gcx.read().await.threads_subscription_restart_flag.clone();
@@ -81,12 +85,12 @@ pub async fn watch_threads_subscription(gcx: Arc>) {
tokio::time::sleep(Duration::from_secs(RECONNECT_DELAY_SECONDS)).await;
continue;
};
-
+
info!(
"starting subscription for threads_in_group with fgroup_id=\"{}\"",
located_fgroup_id
);
- let connection_result = initialize_connection(gcx.clone()).await;
+ let connection_result = initialize_connection(&address_url, &api_key, &located_fgroup_id).await;
let mut connection = match connection_result {
Ok(conn) => conn,
Err(err) => {
@@ -96,18 +100,24 @@ pub async fn watch_threads_subscription(gcx: Arc>) {
continue;
}
};
-
- let events_result = events_loop(gcx.clone(), &mut connection).await;
+
+ let events_result = actual_subscription_loop(
+ gcx.clone(),
+ &mut connection,
+ &address_url,
+ &api_key,
+ &located_fgroup_id
+ ).await;
if let Err(err) = events_result {
error!("failed to process events: {}", err);
info!("will attempt to reconnect in {} seconds", RECONNECT_DELAY_SECONDS);
}
-
+
if gcx.read().await.shutdown_flag.load(Ordering::SeqCst) {
info!("shutting down threads subscription");
break;
}
-
+
let restart_flag = gcx.read().await.threads_subscription_restart_flag.clone();
if !restart_flag.load(Ordering::SeqCst) {
tokio::time::sleep(Duration::from_secs(RECONNECT_DELAY_SECONDS)).await;
@@ -115,7 +125,11 @@ pub async fn watch_threads_subscription(gcx: Arc>) {
}
}
-async fn initialize_connection(gcx: Arc>) -> Result<
+pub async fn initialize_connection(
+ cmd_address_url: &str,
+ api_key: &str,
+ located_fgroup_id: &str,
+) -> Result<
futures::stream::SplitStream<
tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream
@@ -123,12 +137,7 @@ async fn initialize_connection(gcx: Arc>) -> Result<
>,
String,
> {
- let (api_key, located_fgroup_id) = {
- let gcx_read = gcx.read().await;
- (gcx_read.cmdline.api_key.clone(),
- gcx_read.active_group_id.clone().unwrap_or_default())
- };
- let url = Url::parse(crate::constants::GRAPHQL_WS_URL)
+ let url = Url::parse(&crate::constants::get_graphql_ws_url(cmd_address_url))
.map_err(|e| format!("Failed to parse WebSocket URL: {}", e))?;
let mut request = url
.into_client_request()
@@ -161,8 +170,7 @@ async fn initialize_connection(gcx: Arc>) -> Result<
let response: Value = serde_json::from_str(&text)
.map_err(|e| format!("Failed to parse connection response: {}", e))?;
if let Some(msg_type) = response["type"].as_str() {
- if msg_type == "connection_ack" {
- } else if msg_type == "connection_error" {
+ if msg_type == "connection_ack" {} else if msg_type == "connection_error" {
return Err(format!("Connection error: {}", response));
} else {
return Err(format!("Expected connection_ack, got: {}", response));
@@ -195,7 +203,7 @@ async fn initialize_connection(gcx: Arc>) -> Result<
return Err("No response received for connection initialization".to_string());
}
let subscription_message = json!({
- "id": "42",
+ "id": generate_random_hash(16),
"type": "start",
"payload": {
"query": THREADS_SUBSCRIPTION_QUERY,
@@ -211,22 +219,26 @@ async fn initialize_connection(gcx: Arc>) -> Result<
Ok(read)
}
-async fn events_loop(
+async fn actual_subscription_loop(
gcx: Arc>,
connection: &mut futures::stream::SplitStream<
tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream,
>,
>,
+ cmd_address_url: &str,
+ api_key: &str,
+ located_fgroup_id: &str,
) -> Result<(), String> {
info!("cloud threads subscription started, waiting for events...");
- let basic_info = get_basic_info(gcx.clone()).await?;
+ let app_searchable_id = gcx.read().await.app_searchable_id.clone();
+ let basic_info = get_basic_info(cmd_address_url, api_key).await?;
while let Some(msg) = connection.next().await {
- if gcx.read().await.shutdown_flag.load(Ordering::SeqCst) {
+ if gcx.clone().read().await.shutdown_flag.load(Ordering::SeqCst) {
info!("shutting down threads subscription");
break;
}
- if gcx.read().await.threads_subscription_restart_flag.load(Ordering::SeqCst) {
+ if gcx.clone().read().await.threads_subscription_restart_flag.load(Ordering::SeqCst) {
info!("restart flag detected, restarting threads subscription");
return Ok(());
}
@@ -239,8 +251,7 @@ async fn events_loop(
continue;
}
};
- let response_type = response["type"].as_str().unwrap_or("unknown");
- match response_type {
+ match response["type"].as_str().unwrap_or("unknown") {
"data" => {
if let Some(payload) = response["payload"].as_object() {
let data = &payload["data"];
@@ -250,14 +261,20 @@ async fn events_loop(
continue;
}
if let Ok(payload) = serde_json::from_value::(threads_in_group["news_payload"].clone()) {
- match process_thread_event(gcx.clone(), &payload, &basic_info).await {
- Ok(_) => {}
- Err(err) => {
- error!("failed to process thread event: {}", err);
- }
- }
+ let gcx_clone = gcx.clone();
+ let payload_clone = payload.clone();
+ let basic_info_clone = basic_info.clone();
+ let cmd_address_url_clone = cmd_address_url.to_string();
+ let api_key_clone = api_key.to_string();
+ let app_searchable_id_clone = app_searchable_id.clone();
+ let located_fgroup_id_clone = located_fgroup_id.to_string();
+ tokio::spawn(async move {
+ crate::cloud::threads_processing::process_thread_event(
+ gcx_clone, payload_clone, basic_info_clone, cmd_address_url_clone, api_key_clone, app_searchable_id_clone, located_fgroup_id_clone
+ ).await
+ });
} else {
- info!("failed to parse thread payload: {}", text);
+ info!("failed to parse thread payload: {:?}", threads_in_group);
}
} else {
info!("received data message but couldn't find payload");
@@ -265,6 +282,11 @@ async fn events_loop(
}
"error" => {
error!("threads subscription error: {}", text);
+ return Err(format!("{}", text));
+ }
+ "complete" => {
+ error!("threads subscription complete: {}.\nRestarting it", text);
+ return Err(format!("{}", text));
}
_ => {
info!("received message with unknown type: {}", text);
@@ -283,17 +305,8 @@ async fn events_loop(
}
Ok(())
}
-fn generate_random_hash(length: usize) -> String {
- thread_rng()
- .sample_iter(&Alphanumeric)
- .take(length)
- .map(char::from)
- .collect()
-}
-async fn get_basic_info(gcx: Arc>) -> Result {
- let client = Client::new();
- let api_key = gcx.read().await.cmdline.api_key.clone();
+pub async fn get_basic_info(cmd_address_url: &str, api_key: &str) -> Result {
let query = r#"
query GetBasicInfo {
query_basic_stuff {
@@ -307,206 +320,20 @@ async fn get_basic_info(gcx: Arc>) -> Result>,
- thread_payload: &ThreadPayload,
- basic_info: &BasicStuff
-) -> Result<(), String> {
- if thread_payload.ft_need_tool_calls == -1 || thread_payload.owner_fuser_id != basic_info.fuser_id {
- return Ok(());
- }
- let app_searchable_id = gcx.read().await.app_searchable_id.clone();
- if let Some(ft_app_searchable) = thread_payload.ft_app_searchable.clone() {
- if ft_app_searchable != app_searchable_id {
- info!("thread `{}` has different `app_searchable` id, skipping it", thread_payload.ft_id);
- }
- } else {
- info!("thread `{}` doesn't have the `app_searchable` id, skipping it", thread_payload.ft_id);
- return Ok(());
- }
- if let Some(error) = thread_payload.ft_error.as_ref() {
- info!("thread `{}` has the error: `{}`. Skipping it", thread_payload.ft_id, error);
- return Ok(());
- }
- let messages = crate::cloud::messages_req::get_thread_messages(
- gcx.clone(),
- &thread_payload.ft_id,
- thread_payload.ft_need_tool_calls,
- ).await?;
- if messages.is_empty() {
- info!("thread `{}` has no messages. Skipping it", thread_payload.ft_id);
- return Ok(());
- }
- let thread = crate::cloud::threads_req::get_thread(gcx.clone(), &thread_payload.ft_id).await?;
- let hash = generate_random_hash(16);
- match lock_thread(gcx.clone(), &thread.ft_id, &hash).await {
- Ok(_) => {}
- Err(err) => return Err(err)
- }
- let result = if messages.iter().all(|x| x.ftm_role != "system") {
- initialize_thread(gcx.clone(), &thread.ft_fexp_name, &thread, &messages).await
- } else {
- call_tools(gcx.clone(), &thread, &messages).await
+ let config = GraphQLRequestConfig {
+ address: cmd_address_url.to_string(),
+ api_key: api_key.to_string(),
+ user_agent: Some("refact-lsp".to_string()),
+ additional_headers: None,
};
- match crate::cloud::threads_req::unlock_thread(gcx.clone(), thread.ft_id.clone(), hash).await {
- Ok(_) => info!("thread `{}` unlocked successfully", thread.ft_id),
- Err(err) => error!("failed to unlock thread `{}`: {}", thread.ft_id, err),
- }
- result
-}
-async fn initialize_thread(
- gcx: Arc>,
- expert_name: &str,
- thread: &Thread,
- thread_messages: &Vec,
-) -> Result<(), String> {
- let expert = crate::cloud::experts_req::get_expert(gcx.clone(), expert_name).await?;
- let tools: Vec> =
- crate::tools::tools_list::get_available_tools(gcx.clone())
- .await
- .into_iter()
- .filter(|tool| expert.is_tool_allowed(&tool.tool_description().name))
- .collect();
- let tool_descriptions = tools
- .iter()
- .map(|x| x.tool_description().into_openai_style())
- .collect::>();
- crate::cloud::threads_req::set_thread_toolset(gcx.clone(), &thread.ft_id, tool_descriptions).await?;
- let updated_system_prompt = crate::scratchpads::chat_utils_prompts::system_prompt_add_extra_instructions(
- gcx.clone(), expert.fexp_system_prompt.clone(), HashSet::new()
- ).await;
- let last_message = thread_messages.last().unwrap();
- let output_thread_messages = vec![ThreadMessage {
- ftm_belongs_to_ft_id: last_message.ftm_belongs_to_ft_id.clone(),
- ftm_alt: last_message.ftm_alt.clone(),
- ftm_num: 0,
- ftm_prev_alt: 100,
- ftm_role: "system".to_string(),
- ftm_content: Some(
- serde_json::to_value(ChatContent::SimpleText(updated_system_prompt)).unwrap(),
- ),
- ftm_tool_calls: None,
- ftm_call_id: "".to_string(),
- ftm_usage: None,
- ftm_created_ts: std::time::SystemTime::now()
- .duration_since(std::time::SystemTime::UNIX_EPOCH)
- .unwrap()
- .as_secs_f64(),
- ftm_provenance: json!({"important": "information"}),
- }];
- crate::cloud::messages_req::create_thread_messages(
- gcx.clone(),
- &thread.ft_id,
- output_thread_messages,
- ).await?;
- Ok(())
-}
-
-async fn call_tools(
- gcx: Arc>,
- thread: &Thread,
- thread_messages: &Vec,
-) -> Result<(), String> {
- let max_new_tokens = 8192;
- let last_message_num = thread_messages.iter().map(|x| x.ftm_num).max().unwrap_or(0);
- let (alt, prev_alt) = thread_messages
- .last()
- .map(|msg| (msg.ftm_alt, msg.ftm_prev_alt))
- .unwrap_or((0, 0));
- let messages = crate::cloud::messages_req::convert_thread_messages_to_messages(thread_messages);
- let caps = crate::global_context::try_load_caps_quickly_if_not_present(gcx.clone(), 0)
- .await
- .map_err_to_string()?;
- let model_rec = crate::caps::resolve_chat_model(caps, &format!("refact/{}", thread.ft_model))
- .map_err(|e| format!("Failed to resolve chat model: {}", e))?;
- let ccx = Arc::new(AMutex::new(
- AtCommandsContext::new(
- gcx.clone(),
- model_rec.base.n_ctx,
- 12,
- false,
- messages.clone(),
- thread.ft_id.to_string(),
- false,
- thread.ft_model.to_string(),
- ).await,
- ));
- let allowed_tools = crate::cloud::messages_req::get_tool_names_from_openai_format(&thread.ft_toolset).await?;
- let mut all_tools: IndexMap