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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ base64 = "0.22"
tree-sitter = "0.22"
tree-sitter-python = "0.21"
once_cell = "1"
qernel-vision = { path = "vision", version = "0.1.0-alpha" }

[dev-dependencies]
tempfile = "3"
Expand All @@ -50,6 +51,7 @@ members = [
".",
"src/exe/apply-patch",
"src/exe/core",
"vision",
]

[workspace.package]
Expand Down
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,9 @@ You can also output results of the file to Markdown by adding the `--markdown` f

### Limitations

- This project currently relies on AI models that are not optimized for quantum computing concepts/programming, and therefore may not always produce accurate results. **We are actively working to solve this issue.** However, we've seen strong potential in AI models to mathetmatically reason (see [here](https://deepmind.google/discover/blog/advanced-version-of-gemini-with-deep-think-officially-achieves-gold-medal-standard-at-the-international-mathematical-olympiad/), [here](https://x.com/alexwei_/status/1946477742855532918)), and expect this accuracy gap to decrease over time.
- The core infrastructure logic to edit and maintain files in a repository was ported over from the [Codex CLI](https://github.com/openai/codex), and as a result, currently only works with OpenAI models. The [main agent loop]() natively supports the `codex-mini-latest` and `gpt-5-codex` models, if you'd like to use another model, you might need to edit the code and rebuild until we extend the support.
- You currently need to use your own OpenAI API key to access the models, you can create an account and get one from the [OpenAI API Platform site](https://platform.openai.com/docs/overview).
- We're actively working to migrate away from the OpenAI API to [Ollama](https://ollama.com), which will allow you to run your own models locally on your computer, access a suite of open source models, or use a cloud model if you wish.

- This project currently relies on both open and closed-source models that are not optimized for quantum computing concepts/programming, and therefore may not always produce accurate results. **We are actively working to solve this issue.** However, we've seen strong potential in AI models to mathetmatically reason (see [here](https://deepmind.google/discover/blog/advanced-version-of-gemini-with-deep-think-officially-achieves-gold-medal-standard-at-the-international-mathematical-olympiad/), [here](https://x.com/alexwei_/status/1946477742855532918)), and expect this accuracy gap to decrease over time.

### Tips for best performance

- For one-shot prototyping of arXiv papers, it's highly recommended to use `gpt-5-codex` with detailed implementation notes in `spec.md`. The model is surprisingly good if you tell it exactly what you want. The drawbacks are that it's expensive.
- For smaller examples that do not involve high context, `codex-mini-latest` with a well formatted spec file and well written tests can often get the job done.
- The agents looks at the tests in `src/tests.py` to form its implementation, and will automatically run its solutions against the test suite upon each iteration to mold its implementation. You can set the `--max-iter` flag to limit how many times it tries. Simple tests can significantly improve and speed up the implemetation process.

### Cloning and sharing projects
Expand All @@ -142,3 +135,6 @@ qernel push <URL>
qernel pull <REPO> <DEST>
```

### Acknowledgements
- The core infrastructure logic to edit and maintain files in a repository was ported over from the [Codex CLI](https://github.com/openai/codex). Thank you OpenAI team for making this public and under a permissive license.
- Thank you [Linus Torvalds](https://en.wikipedia.org/wiki/Linus_Torvalds) for building the foundation of modern software, and serving as an inspiration an example for the power of open source work.
2 changes: 2 additions & 0 deletions src/cmd/explain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ pub mod chunk;
pub mod prompts;
pub mod renderer;
mod network;
mod preflight;

pub use run::handle_explain;
pub use preflight::check_explain;


134 changes: 88 additions & 46 deletions src/cmd/explain/network.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,99 @@
use anyhow::{Context, Result};
use serde_json::json;
use crate::common::network::{default_client, detect_provider, parse_ollama_text, parse_model_text, ProviderKind, ollama_chat_url, qernel_model_url, preflight_check};
use crate::util::get_qernel_pat_from_env_or_config;

pub fn call_text_model(api_key: &str, model: &str, system: &str, user: &str) -> Result<String> {
use reqwest::blocking::Client;
if api_key.is_empty() { anyhow::bail!("OPENAI_API_KEY is empty"); }
let client = Client::builder()
.timeout(std::time::Duration::from_secs(300))
.build()
.context("create http client")?;

// Use Responses API for consistency with existing code
let input = vec![
json!({"role":"system","content":system}),
json!({"role":"user","content":user}),
];

let resp = client
.post("https://api.openai.com/v1/responses")
.bearer_auth(api_key)
.json(&json!({
"model": model,
"input": input,
"parallel_tool_calls": false
}))
.send()
.context("send openai request")?;

let status = resp.status();
let text = resp.text().unwrap_or_default();
if !status.is_success() {
anyhow::bail!("OpenAI error {}: {}", status, text);
}
let body: serde_json::Value = serde_json::from_str(&text).context("parse openai json")?;
let provider = detect_provider();
let use_ollama = provider == ProviderKind::Ollama;
let use_qernel = provider == ProviderKind::Qernel;

// Prefer output_text, else join message content
if let Some(s) = body.get("output_text").and_then(|v| v.as_str()) {
return Ok(s.to_string());
}
if let Some(arr) = body.get("output").and_then(|v| v.as_array()) {
// Try to concatenate text parts
let mut buf = String::new();
for item in arr {
if item.get("type").and_then(|v| v.as_str()) == Some("message") {
if let Some(parts) = item.get("content").and_then(|v| v.as_array()) {
for p in parts {
if let Some(t) = p.get("text").and_then(|t| t.as_str()) { buf.push_str(t); }
}
}
}
if !(use_ollama || use_qernel) && api_key.is_empty() { anyhow::bail!("OPENAI_API_KEY is empty"); }
let client: Client = default_client(300)?;
preflight_check(&client, provider, model)?;

if use_ollama {
// Ollama via OpenAI-compatible Chat Completions API
let url = ollama_chat_url();
let messages = vec![
json!({"role": "system", "content": system}),
json!({"role": "user", "content": user}),
];

let resp = client
.post(&url)
.json(&json!({
"model": model,
"messages": messages,
"stream": false
}))
.send()
.context("send ollama chat request")?;

let status = resp.status();
let text = resp.text().unwrap_or_default();
if !status.is_success() {
anyhow::bail!("Ollama error {}: {}", status, text);
}
let body: serde_json::Value = serde_json::from_str(&text).context("parse ollama json")?;
if let Some(s) = parse_ollama_text(&body) { return Ok(s); }
anyhow::bail!("No text in Ollama response")
} else if use_qernel {
// Qernel model endpoint: minimal Responses-like payload
let input = vec![
json!({"role":"system","content":system}),
json!({"role":"user","content":user}),
];
let url = qernel_model_url();
let mut req = client.post(&url);
if let Some(pat) = get_qernel_pat_from_env_or_config() {
req = req.bearer_auth(pat);
}
let resp = req
.json(&json!({
"model": model,
"input": input
}))
.send()
.context("send qernel request")?;

let status = resp.status();
let text = resp.text().unwrap_or_default();
if !status.is_success() {
anyhow::bail!("Qernel error {}: {}", status, text);
}
let body: serde_json::Value = serde_json::from_str(&text).context("parse qernel json")?;
if let Some(s) = parse_model_text(&body) { return Ok(s); }
anyhow::bail!("No text in Qernel response")
} else {
// OpenAI Responses API (existing behavior)
let input = vec![
json!({"role":"system","content":system}),
json!({"role":"user","content":user}),
];

// Default path: Qernel Responses-compatible endpoint
let url = qernel_model_url();
let resp = client
.post(&url)
.json(&json!({
"model": model,
"input": input,
"parallel_tool_calls": false
}))
.send()
.context("send qernel request")?;

let status = resp.status();
let text = resp.text().unwrap_or_default();
if !status.is_success() {
anyhow::bail!("Qernel error {}: {}", status, text);
}
if !buf.is_empty() { return Ok(buf); }
let body: serde_json::Value = serde_json::from_str(&text).context("parse qernel json")?;
if let Some(s) = parse_model_text(&body) { return Ok(s); }
anyhow::bail!("No text in Qernel response")
}
anyhow::bail!("No text in OpenAI response")
}


46 changes: 46 additions & 0 deletions src/cmd/explain/preflight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use anyhow::Result;
use std::path::PathBuf;

use crate::config::load_config as load_proj_config;

pub fn check_explain(files: Vec<String>, _model: Option<String>) -> Result<()> {
if files.is_empty() { anyhow::bail!("no files provided"); }
// Ensure files exist
for f in &files {
let p = std::path::Path::new(f);
if !p.exists() { anyhow::bail!("file not found: {}", f); }
}

// Resolve model from project config vs CLI
let cwd = std::env::current_dir().unwrap_or(PathBuf::from("."));
let proj_cfg_path = cwd.join(".qernel").join("qernel.yaml");
let configured_model = if proj_cfg_path.exists() {
let default_model = crate::util::get_default_explain_model();
let yaml_model = load_proj_config(&proj_cfg_path)
.ok()
.and_then(|c| c.explain_model);
if let Some(y) = yaml_model.as_ref() {
if y != &default_model {
println!(
"Warning: YAML explain_model '{}' differs from tool default '{}'. YAML takes precedence at runtime.",
y, default_model
);
}
}
yaml_model.unwrap_or(default_model)
} else {
crate::util::get_default_explain_model()
};
// For --check, always use configured precedence (YAML -> tool default),
// ignoring the CLI default model value to avoid accidental overrides.
let effective_model = configured_model;

// Provider preflight
let client = crate::common::network::default_client(10)?;
let provider = crate::common::network::detect_provider();
crate::common::network::preflight_check(&client, provider, &effective_model)?;
println!("Explain preflight passed for model '{}'.", effective_model);
Ok(())
}


50 changes: 39 additions & 11 deletions src/cmd/explain/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ use std::path::PathBuf;
use super::chunk::{ChunkGranularity, PythonChunk, chunk_python_or_fallback};
use super::prompts::build_snippet_prompt;
use super::network::call_text_model;
use crate::util::get_openai_api_key_from_env_or_config;
use super::renderer::{render_console, render_markdown_report, RenderOptions};
use std::io::{self, Write};
// use std::path::Path; // unused
use crate::config::load_config as load_proj_config;
use serde::Deserialize;
use indicatif::{ProgressBar, ProgressStyle};

Expand All @@ -15,7 +17,7 @@ struct SnippetSummary { id: String, summary: String }
pub fn handle_explain(
files: Vec<String>,
per: String,
model: String,
model: Option<String>,
markdown: bool,
output: Option<String>,
pager: bool,
Expand Down Expand Up @@ -43,6 +45,31 @@ pub fn handle_explain(

if let Some(dir) = output_dir.as_ref() { std::fs::create_dir_all(dir).ok(); }

// Resolve effective model: prefer project config's explain_model unless CLI explicitly overrides and user confirms
let cwd = std::env::current_dir().unwrap_or(PathBuf::from("."));
let proj_cfg_path = cwd.join(".qernel").join("qernel.yaml");
let configured_model = if proj_cfg_path.exists() {
load_proj_config(&proj_cfg_path)
.ok()
.and_then(|c| c.explain_model)
.unwrap_or_else(|| crate::util::get_default_explain_model())
} else {
crate::util::get_default_explain_model()
};

let effective_model = match model.as_ref() {
Some(cli_model) if cli_model != &configured_model => {
if ask_confirm(&format!(
"Configured explain model is '{}'. Override with CLI model '{}'? [y/N]: ",
configured_model, cli_model
))? { cli_model.clone() } else { configured_model.clone() }
}
Some(cli_model) => cli_model.clone(),
None => configured_model.clone(),
};

println!("Explain model: {}", effective_model);

// For now, sequential per file; we can parallelize later with a concurrency cap.
for file in files {
let path = PathBuf::from(&file);
Expand All @@ -58,7 +85,6 @@ pub fn handle_explain(
let snippets: Vec<PythonChunk> = chunk_python_or_fallback(&content, &path, granularity)?;

// Concurrent per-snippet calls (bounded)
let api_key = get_openai_api_key_from_env_or_config().unwrap_or_default();
let max_workers = std::env::var("QERNEL_EXPLAIN_WORKERS").ok().and_then(|s| s.parse::<usize>().ok()).unwrap_or(4);

let mut handles: Vec<std::thread::JoinHandle<(usize, String)>> = Vec::new();
Expand All @@ -83,14 +109,9 @@ pub fn handle_explain(
}
}

let model_cl = model.clone();
let api_key_cl = api_key.clone();
let model_cl = effective_model.clone();
let handle = std::thread::spawn(move || {
let text = if api_key_cl.is_empty() {
super::prompts::mock_call_model(&model_cl, &system, &user).unwrap_or_else(|_| "(mock explanation)".to_string())
} else {
call_text_model(&api_key_cl, &model_cl, &system, &user).unwrap_or_else(|e| format!("(error: {})", e))
};
let text = call_text_model("", &model_cl, &system, &user).unwrap_or_else(|e| format!("(error: {})", e));
(idx, text)
});
handles.insert(0, handle);
Expand Down Expand Up @@ -126,4 +147,11 @@ pub fn handle_explain(
Ok(())
}


fn ask_confirm(prompt: &str) -> Result<bool> {
print!("{}", prompt);
io::stdout().flush().ok();
let mut buf = String::new();
io::stdin().read_line(&mut buf).ok();
let ans = buf.trim().to_lowercase();
Ok(ans == "y" || ans == "yes")
}
10 changes: 2 additions & 8 deletions src/cmd/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use indicatif::{ProgressBar, ProgressStyle};
use std::env;
use std::io::{self, Read};

use crate::util::{load_config, save_config, get_openai_api_key_from_env_or_config, set_openai_api_key_in_config, unset_openai_api_key_in_config};
use crate::util::{load_config, save_config, set_openai_api_key_in_config, unset_openai_api_key_in_config};
use owo_colors::OwoColorize;
use reqwest::blocking::Client;
use serde::Deserialize;
Expand Down Expand Up @@ -44,13 +44,7 @@ pub fn handle_auth_with_flags(set_openai_key: bool, unset_openai_key: bool) -> R
let masked = if token.len() > 8 { format!("{}...", &token[..8]) } else { "...".to_string() };
println!("{} Personal access token: {}", crate::util::sym_check(ce), masked.blue().bold());
// Also surface OpenAI key status
let has_openai = get_openai_api_key_from_env_or_config().is_some();
if has_openai {
println!("{} OpenAI API key detected. Note: prototyping uses OpenAI today; we're migrating to Ollama/open-source models soon.", crate::util::sym_check(ce));
} else {
println!("{} Warning: No OpenAI API key detected. Prototyping features won't be available until a key is set.", crate::util::sym_question(ce));
println!(" You can set one with: qernel auth --set-openai-key");
}
// Surface of API key status moved to `qernel provider --show`

if let Ok(client) = Client::builder().timeout(std::time::Duration::from_secs(10)).build() {
if let Ok(r) = client
Expand Down
2 changes: 2 additions & 0 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ pub mod push;
pub mod pull;
pub mod prototype;
pub mod explain;
pub mod see;
pub mod provider;

1 change: 1 addition & 0 deletions src/cmd/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Implement the algorithms and concepts described in the research paper.
benchmarks: crate::config::BenchmarkConfig {
test_command: "python -m pytest src/tests.py -v".to_string(),
},
explain_model: Some("codex-mini-latest".to_string()),
};

save_config(&config, &qernel_dir.join("qernel.yaml"))?;
Expand Down
Loading