Skip to content

Commit 44b0039

Browse files
committed
Add dummy provider pipeline
1 parent 18520e3 commit 44b0039

6 files changed

Lines changed: 217 additions & 24 deletions

File tree

PROJEKT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ CompText CLI is an experimental terminal context client for building determinist
2222
| **Phase 0** | Repo Genesis & Bootstrap | Scaffold Rust project, basic commands (`help`, `doctor`, `providers list`, `version`), verify CI | **COMPLETE** |
2323
| **Phase 1** | CLI Shell Hardening | Hardening argument parsing, input handling, and errors for the base shell commands | **COMPLETE** |
2424
| **Phase 2** | Context Pack Contract | Implement `ctxt context inspect`, `ctxt context pack --task "..."`, and `ctxt ask --dry-run "..."` | **COMPLETE** |
25-
| **Phase 3** | Provider Adapter Layer | Define provider interface and Dummy offline test provider | *PLANNED* |
25+
| **Phase 3** | Provider Adapter Layer | Define provider interface and Dummy offline test provider | **COMPLETE** |
2626
| **Phase 4** | Ollama Local Adapter | Support local Ollama integrations with explicit network boundaries | *PLANNED* |
2727
| **Phase 5** | Proposal Apply Gate | Implement proposal files, approval checks, and validation flow | *PLANNED* |
2828

reports/phase_3_status.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# CompText CLI — Phase 3 Status Report
2+
3+
This report documents the implementation and verification details of Phase 3 (Dummy Provider integration).
4+
5+
## 1. Objectives & Scope
6+
- **Goal**: Implement the offline dummy provider pipeline to support request/response dry-run execution without requiring network or live API keys.
7+
- **Trigger Command**: `ctxt ask --provider dummy "<PROMPT>"`
8+
- **Deliverables**:
9+
- Normalized `Provider` trait (interface).
10+
- Concrete `DummyProvider` implementation producing deterministic responses.
11+
- Integration of provider execution workflow into `src/cli.rs`.
12+
- Offline tests ensuring proper request wrapping and serialization.
13+
14+
## 2. Implementation Summary
15+
- **Provider Interface (`src/provider.rs`)**:
16+
Defines `Provider` with:
17+
- `fn name(&self) -> &str`
18+
- `fn execute(&self, request: &ModelRequest) -> Result<ModelResponse, String>`
19+
- **Dummy Adapter (`src/provider.rs`)**:
20+
- Analyzes the `ModelRequest` content (system prompt) to determine the number of files packed.
21+
- Outputs a deterministic markdown/text answer referencing the user prompt and context pack size.
22+
- **CLI Command Matching**:
23+
- Enhanced command parsing in `src/cli.rs` to allow optional parameter parsing (e.g. `--provider <name>` and `--dry-run`).
24+
- Serializes and stores final LLM answers into `.comptext/model_response.latest.json` when run without `--dry-run`.
25+
- Re-uses existing context pack contract outputs.
26+
27+
## 3. Local Verification Results
28+
- `cargo fmt --all --check` (Passed)
29+
- `cargo check` (Passed)
30+
- `cargo test` (Passed 14 test cases total, including `ask_dummy_provider_succeeds`)
31+
- `cargo clippy -- -D warnings` (Passed)
32+
33+
Run Verification Command:
34+
```bash
35+
cargo run --bin ctxt -- ask --provider dummy "How do I test this repo?"
36+
```
37+
Stdout:
38+
```text
39+
Response from dummy provider:
40+
Mock LLM response from CompText Dummy Provider.
41+
Received prompt: "How do I test this repo?"
42+
Workspace context analyzed successfully: 45 files included.
43+
Dummy status: offline-test-provider ok.
44+
```
45+
46+
## 4. Safety & Claims Hygiene
47+
- Network access remained completely denied.
48+
- No environment variables or credentials were read or dumped.
49+
- No production readiness or official compliance assertions were made.

src/cli.rs

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ enum Command {
99
Doctor,
1010
ProvidersList,
1111
ContextInspect,
12-
ContextPack { task: String },
13-
Ask { dry_run: bool, prompt: String },
12+
ContextPack {
13+
task: String,
14+
},
15+
Ask {
16+
provider: Option<String>,
17+
dry_run: bool,
18+
prompt: String,
19+
},
1420
}
1521

1622
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -87,7 +93,11 @@ where
8793
1
8894
}
8995
},
90-
Ok(Command::Ask { dry_run, prompt }) => match handle_ask(dry_run, &prompt) {
96+
Ok(Command::Ask {
97+
provider,
98+
dry_run,
99+
prompt,
100+
}) => match handle_ask(provider.as_deref(), dry_run, &prompt) {
91101
Ok(_) => 0,
92102
Err(e) => {
93103
eprintln!("error: {e}");
@@ -189,22 +199,53 @@ fn parse(argv: &[String]) -> Result<Command, String> {
189199
"ask" => {
190200
if argv.len() < 3 {
191201
return Err(
192-
"missing arguments for 'ask'. Usage: ctxt ask --dry-run \"<prompt>\""
202+
"missing arguments for 'ask'. Usage: ctxt ask --dry-run \"<prompt>\" or ctxt ask --provider <provider> \"<prompt>\""
193203
.to_string(),
194204
);
195205
}
196-
if argv[1] != "--dry-run" {
197-
return Err(format!(
198-
"unexpected option '{}' for 'ask'. Expected '--dry-run'",
199-
argv[1]
200-
));
206+
207+
let mut provider = None;
208+
let mut dry_run = false;
209+
let mut prompt = String::new();
210+
211+
let mut i = 1;
212+
while i < argv.len() {
213+
match argv[i].as_str() {
214+
"--dry-run" => {
215+
dry_run = true;
216+
i += 1;
217+
}
218+
"--provider" => {
219+
if i + 1 >= argv.len() {
220+
return Err("missing provider name after --provider".to_string());
221+
}
222+
provider = Some(argv[i + 1].clone());
223+
i += 2;
224+
}
225+
other => {
226+
if other.starts_with('-') {
227+
return Err(format!("unsupported option '{other}' for 'ask'"));
228+
}
229+
if !prompt.is_empty() {
230+
return Err(format!("unexpected argument '{other}' for 'ask'"));
231+
}
232+
prompt = other.to_string();
233+
i += 1;
234+
}
235+
}
201236
}
202-
let prompt = argv[2].clone();
203-
if argv.len() > 3 {
204-
return Err(format!("unexpected argument '{}' for 'ask'", argv[3]));
237+
238+
if prompt.is_empty() {
239+
return Err("missing prompt for 'ask'".to_string());
240+
}
241+
242+
if !dry_run && provider.is_none() {
243+
return Err("must specify either --dry-run or --provider <provider>".to_string());
205244
}
245+
206246
Ok(Command::Ask {
207-
dry_run: true,
247+
provider,
248+
dry_run,
208249
prompt,
209250
})
210251
}
@@ -410,11 +451,7 @@ fn handle_context_pack(task: &str) -> Result<(), String> {
410451
Ok(())
411452
}
412453

413-
fn handle_ask(dry_run: bool, prompt: &str) -> Result<(), String> {
414-
if !dry_run {
415-
return Err("Only dry-run is supported. Use --dry-run flag.".to_string());
416-
}
417-
454+
fn handle_ask(provider: Option<&str>, dry_run: bool, prompt: &str) -> Result<(), String> {
418455
let cp = build_context_pack(prompt)?;
419456
std::fs::create_dir_all(".comptext")
420457
.map_err(|e| format!("failed to create .comptext directory: {e}"))?;
@@ -430,7 +467,7 @@ fn handle_ask(dry_run: bool, prompt: &str) -> Result<(), String> {
430467
cp.rendered_context
431468
);
432469
let request = ModelRequest {
433-
provider: "dummy".to_string(),
470+
provider: provider.unwrap_or("dummy").to_string(),
434471
model: "dummy-model".to_string(),
435472
messages: vec![
436473
Message {
@@ -450,10 +487,32 @@ fn handle_ask(dry_run: bool, prompt: &str) -> Result<(), String> {
450487
std::fs::write(".comptext/model_request.latest.json", req_json)
451488
.map_err(|e| format!("failed to write model request: {e}"))?;
452489

453-
println!("Dry-run successful.");
454-
println!("Context Pack: .comptext/context_pack.latest.json");
455-
println!("Model Request: .comptext/model_request.latest.json");
456-
Ok(())
490+
if dry_run {
491+
println!("Dry-run successful.");
492+
println!("Context Pack: .comptext/context_pack.latest.json");
493+
println!("Model Request: .comptext/model_request.latest.json");
494+
return Ok(());
495+
}
496+
497+
let p_name = provider.ok_or_else(|| "provider is required for live execution".to_string())?;
498+
match p_name {
499+
"dummy" => {
500+
use crate::provider::{DummyProvider, Provider};
501+
let prov = DummyProvider;
502+
let response = prov.execute(&request)?;
503+
504+
let resp_json = serde_json::to_string_pretty(&response)
505+
.map_err(|e| format!("failed to serialize model response: {e}"))?;
506+
507+
std::fs::write(".comptext/model_response.latest.json", resp_json)
508+
.map_err(|e| format!("failed to write model response: {e}"))?;
509+
510+
println!("Response from {} provider:", prov.name());
511+
println!("{}", response.content);
512+
Ok(())
513+
}
514+
other => Err(format!("unsupported provider '{other}'")),
515+
}
457516
}
458517

459518
#[cfg(test)]
@@ -519,12 +578,30 @@ mod tests {
519578
assert_eq!(
520579
parse(&s(&["ask", "--dry-run", "How do I test this repo?"])),
521580
Ok(Command::Ask {
581+
provider: None,
522582
dry_run: true,
523583
prompt: "How do I test this repo?".to_string()
524584
})
525585
);
526586
}
527587

588+
#[test]
589+
fn parses_ask_provider() {
590+
assert_eq!(
591+
parse(&s(&[
592+
"ask",
593+
"--provider",
594+
"dummy",
595+
"How do I test this repo?"
596+
])),
597+
Ok(Command::Ask {
598+
provider: Some("dummy".to_string()),
599+
dry_run: false,
600+
prompt: "How do I test this repo?".to_string()
601+
})
602+
);
603+
}
604+
528605
#[test]
529606
fn rejects_unknown_command() {
530607
assert!(parse(&s(&["unknown"])).is_err());

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod cli;
2+
mod provider;
23

34
fn main() {
45
let code = cli::run(std::env::args().skip(1));

src/provider.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use crate::cli::ModelRequest;
2+
use serde::{Deserialize, Serialize};
3+
4+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
5+
pub struct ModelResponse {
6+
pub provider: String,
7+
pub model: String,
8+
pub content: String,
9+
}
10+
11+
pub trait Provider {
12+
fn name(&self) -> &str;
13+
fn execute(&self, request: &ModelRequest) -> Result<ModelResponse, String>;
14+
}
15+
16+
pub struct DummyProvider;
17+
18+
impl Provider for DummyProvider {
19+
fn name(&self) -> &str {
20+
"dummy"
21+
}
22+
23+
fn execute(&self, request: &ModelRequest) -> Result<ModelResponse, String> {
24+
// Count how many files are formatted in the system message context
25+
let file_count = if let Some(sys_msg) = request.messages.iter().find(|m| m.role == "system")
26+
{
27+
sys_msg.content.matches("=== FILE: ").count()
28+
} else {
29+
0
30+
};
31+
32+
let user_prompt = request
33+
.messages
34+
.iter()
35+
.find(|m| m.role == "user")
36+
.map(|m| m.content.as_str())
37+
.unwrap_or("");
38+
39+
let content = format!(
40+
"Mock LLM response from CompText Dummy Provider.\n\
41+
Received prompt: \"{user_prompt}\"\n\
42+
Workspace context analyzed successfully: {file_count} files included.\n\
43+
Dummy status: offline-test-provider ok."
44+
);
45+
46+
Ok(ModelResponse {
47+
provider: "dummy".to_string(),
48+
model: "dummy-model".to_string(),
49+
content,
50+
})
51+
}
52+
}

tests/cli_smoke.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,17 @@ fn providers_include_dummy_and_ollama_variants() {
3030
assert!(stdout.contains("ollama-local"));
3131
assert!(stdout.contains("ollama-cloud-direct"));
3232
}
33+
34+
#[test]
35+
fn ask_dummy_provider_succeeds() {
36+
let stdout = run(&["ask", "--provider", "dummy", "How do I test this repo?"]);
37+
assert!(stdout.contains("Response from dummy provider:"));
38+
assert!(stdout.contains("Mock LLM response from CompText Dummy Provider."));
39+
assert!(stdout.contains("Received prompt: \"How do I test this repo?\""));
40+
41+
// Verify response file was written
42+
let response_path = std::path::Path::new(".comptext/model_response.latest.json");
43+
assert!(response_path.exists());
44+
let response_content = std::fs::read_to_string(response_path).unwrap();
45+
assert!(response_content.contains("\"provider\": \"dummy\""));
46+
}

0 commit comments

Comments
 (0)