Skip to content

Commit

Permalink
Merge pull request #10 from cosmonaut-nz/cli
Browse files Browse the repository at this point in the history
Feat: Added HTML report outputs and gathered the reporting functions into one module
  • Loading branch information
avastmick committed Nov 29, 2023
2 parents 8dae651 + 26978ed commit d539968
Show file tree
Hide file tree
Showing 13 changed files with 585 additions and 181 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ target
# Generated files
/Cargo.lock
**/*.rs.bk
/output/*.json
/output/*.csv
/output

# IDEs and editors
/.idea
Expand Down
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,16 @@ name = "cosmonaut_code"
path = "src/main.rs"

[dependencies]
openai-api-rs = "2.1.3"
openai-api-rs = "2.1.4"
# openai-api-rs = { git = "https://github.com/cosmonaut-nz/openai-api-rs.git"}
thiserror = "1.0.50"
image = "0.24.7"
log = "0.4.20"
env_logger = "0.10.1"
dotenvy = "0.15.7"
inquire = "0.6.2"
tokio = { version = "1.34.0", features = ["full"] }
reqwest = { version = "0.11.22", features = ["json"] }
config = "0.13.1"
serde = { version = "1.0.192", features = ["derive"] }
config = "0.13.4"
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
chrono = { version = "0.4.31", features = ["serde"] }
walkdir = "2.4.0"
Expand All @@ -37,6 +36,7 @@ async-trait = "0.1.74"
git2 = "0.18.1"
# linguist-rs = "1.1.0" # Using direct repository fetch in place of crates.io as version is out of date (local code changes)
linguist-rs = { git = "https://github.com/cosmonaut-nz/linguist-rs.git" }
handlebars = "4.5.0"


[dev-dependencies]
Expand Down
5 changes: 3 additions & 2 deletions settings/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
"name": "OpenAI",
"service": "gpt-4",
"model": "gpt-4-1106-preview",
"api_url": "https://api.openai.com/v1/chat/completions"
"api_url": "https://api.openai.com/v1/chat/completions",
"max_retries": 5
}
],
"chosen_provider": null,
"default_provider": "OpenAI",
"output_type": "json",
"output_type": "html",
"review_type": 1
}
25 changes: 23 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ mod provider;
mod review;
mod settings;
use log::{error, info};
use std::time::{Duration, Instant};
use std::{
process::Command,
time::{Duration, Instant},
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
Expand All @@ -26,9 +29,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
};

// Call the assess_codebase, according to user configuration, either from commandline, or json settings files.
review::assess_codebase(settings).await?;
let report_output = review::assess_codebase(settings).await?;

info!("CODE REVIEW COMPLETE. See the output report for details.");
if let Err(e) = open_file(&report_output) {
error!("Failed to open file: {}", e);
}

print_exec_duration(start.elapsed());
Ok(())
Expand All @@ -46,3 +52,18 @@ fn print_exec_duration(duration: Duration) {
minutes, seconds, millis
);
}
fn open_file(file_path: &str) -> std::io::Result<()> {
if cfg!(target_os = "windows") {
Command::new("cmd")
.args(["/C", "start", file_path])
.spawn()?;
} else if cfg!(target_os = "macos") {
Command::new("open").arg(file_path).spawn()?;
} else if cfg!(target_os = "linux") {
Command::new("xdg-open").arg(file_path).spawn()?;
} else {
println!("Unsupported OS");
}

Ok(())
}
71 changes: 59 additions & 12 deletions src/provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::provider::api::{
};
use crate::provider::prompts::PromptData;
use crate::settings::{ProviderSettings, Settings};
use log::{info, warn};
use openai_api_rs::v1::api::Client;
use openai_api_rs::v1::chat_completion::{ChatCompletionMessage, ChatCompletionRequest};
use serde_json::json;
Expand Down Expand Up @@ -67,6 +68,8 @@ trait APIProvider {
) -> Result<ProviderCompletionResponse, Box<dyn std::error::Error>>;
}

/// Holds a consistent 'seed' value
const SEED_VAL: i64 = 1;
/// Creates an API provider, e.g., 'OpenAI' (using the openai_api_rs crate)
struct OpenAIProvider {
model: String,
Expand All @@ -78,26 +81,70 @@ impl APIProvider for OpenAIProvider {
settings: &Settings,
prompt_data: PromptData,
) -> Result<ProviderCompletionResponse, Box<dyn std::error::Error>> {
settings.sensitive.api_key.use_key(
"from provider::OpenAIProvider::code_review for openai_api_rs::v1::api::Client::new",
|key| {
settings
.sensitive
.api_key
.use_key(|key| {
let client: Client = Client::new(key.to_string());
// Only works on latest versions, as of 2023/12
// TODO: Check that the right model version is being used, if older don't add response_format and seed
let res_format = json!({ "type": "json_object" });
// Enables (mostly) deterministic outputs, beta in API, not in crate
let completion_msgs: Vec<ChatCompletionMessage> =
OpenAIMessageConverter.convert_messages(&prompt_data.messages);
let req: ChatCompletionRequest =
ChatCompletionRequest::new(self.model.to_string(), completion_msgs).response_format(res_format);
ChatCompletionRequest::new(self.model.to_string(), completion_msgs)
.response_format(res_format)
.seed(SEED_VAL);
async move {
match client.chat_completion(req) {
Ok(openai_res) => {
let provider_completion_response: ProviderCompletionResponse =
OpenAIResponseConverter.to_generic_provider_response(&openai_res);
Ok(provider_completion_response)
let mut attempts = 0;
let max_retries = settings
.get_active_provider()
.map_or(0, |provider_settings| {
provider_settings.max_retries.unwrap_or(0)
});
loop {
// TODO nesting so deep right now! Refactor to tidy this up.
match client.chat_completion(req.clone()) {
Ok(openai_res) => {
let provider_completion_response: ProviderCompletionResponse =
OpenAIResponseConverter
.to_generic_provider_response(&openai_res);
return Ok(provider_completion_response);
}
Err(openai_err) => {
attempts += 1;
if attempts >= max_retries {
return Err(format!(
"OpenAI API request failed after {} attempts: {}",
attempts, openai_err
)
.into());
}
if let Some(err_code) = extract_http_status(&openai_err.message) {
if err_code == 502 && attempts < max_retries {
warn!(
"Received 502 error, retrying... (Attempt {} of {})",
attempts, max_retries
);
info!("Retrying request to OpenAI API.");
continue;
}
}
return Err(
format!("OpenAI API request failed: {}", openai_err).into()
);
}
}
Err(openai_err) => Err(format!("OpenAI API request failed: {}", openai_err).into()),
}
}
}
).await
})
.await
}
}
fn extract_http_status(error_message: &str) -> Option<u16> {
if error_message.contains("502") {
return Some(502);
}
None
}
2 changes: 1 addition & 1 deletion src/provider/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const JSON_HANDLING_ADVICE: &str = r#"Provide your analysis in valid JSON format
Strictly escape any characters within your response strings that will create invalid JSON, such as \" - i.e., double quotes.
Never use comments in your JSON.
Ensure that your output exactly conforms to the following JSON Schema
and you follow the instructions provided in "description" fields."#;
and you follow exactly the instructions provided in "description" fields."#;

/// Holds the id and [`Vec`] of [`ProviderCompletionMessage`]s
#[derive(Serialize, Deserialize, Debug)]
Expand Down
2 changes: 1 addition & 1 deletion src/provider/specification/file_review.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"items": {
"$ref": "#/$defs/securityIssue"
},
"description": "A list of security issues, threats or vulnerabilities, such as OWASP, etc. Give the threat (and reference, such as OWASP, etc.) and mitigation for each"
"description": "A list of security issues, threats or vulnerabilities, such as listed by OWASP, etc. Give the threat (and reference, such as listed by OWASP, or any other security frameworks) and mitigation for each"
},
"statistics": {
"$ref": "#/$defs/languageFileType",
Expand Down
2 changes: 1 addition & 1 deletion src/review/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ pub(crate) fn analyse_file_language(
_rules: &RegexSet,
_docs: &RegexSet,
) -> Option<(Language, u64, i64)> {
// TODO: resolve the type of file if docs, dotfile, or config
// TODO: resolve the type of file if docs, dotfile, or config and handle separately, particularly documentation, which needs to be summarised
// if is_vendor(entry.path(), rules)
// || is_documentation(relative_path, docs)
// || is_dotfile(relative_path)
Expand Down
2 changes: 1 addition & 1 deletion src/review/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub(crate) enum RAGStatus {
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub(crate) struct RepositoryReview {
repository_name: String, // Derived from path
pub(crate) repository_name: String, // Derived from path
#[serde(skip_serializing_if = "Option::is_none")]
generative_ai_service_and_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
Loading

0 comments on commit d539968

Please sign in to comment.