feat: add workshop-llm-detector
Signed-off-by: Catty Steve <4795515+Catty2014@user.noreply.gitee.com>
This commit is contained in:
@@ -1 +1,2 @@
|
||||
target
|
||||
playground
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "rust-tongsuo"]
|
||||
path = rust-tongsuo
|
||||
url = https://github.com/Tongsuo-Project/rust-tongsuo.git
|
||||
Submodule
+1
Submodule rust-tongsuo added at 6248690a45
@@ -1 +0,0 @@
|
||||
/target
|
||||
Generated
-7
@@ -1,7 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "workshop-config-manager"
|
||||
version = "0.1.0"
|
||||
@@ -1,6 +0,0 @@
|
||||
[package]
|
||||
name = "workshop-config-manager"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
Generated
+1
-1
@@ -574,7 +574,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "workshop-token"
|
||||
name = "workshop-deviceid"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
extern crate workshop_token;
|
||||
use workshop_token::DeviceIdGenerator;
|
||||
extern crate workshop_deviceid;
|
||||
use workshop_deviceid::DeviceIdGenerator;
|
||||
|
||||
// server/src/auth/device_id_example.rs
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// server/src/auth/device_id_generator.rs
|
||||
// device_id_generator.rs
|
||||
use serde::Serialize;
|
||||
mod device_info;
|
||||
use crate::device_info::{DeviceInfo, DeviceInfoCollector};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
target
|
||||
Generated
+2045
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "workshop-llm-detector"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "workshop-llm-detector"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "workshop_llm_detector"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
async-trait = "0.1.89"
|
||||
clap = { version = "4.5.51", features = ["derive"] }
|
||||
log = "0.4.28"
|
||||
ollama-rs = "0.3.2"
|
||||
openai_api_rust = "0.1.9"
|
||||
reqwest = { version = "0.12.24", features = ["json"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_yaml = "0.9.34"
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
@@ -0,0 +1,29 @@
|
||||
name: "Rust"
|
||||
project_types:
|
||||
- "rust"
|
||||
- "cargo"
|
||||
install_path: "/install"
|
||||
artifacts:
|
||||
- "/install/bin/*"
|
||||
- "/install/lib/*.so"
|
||||
- "/install/lib/*.a"
|
||||
- "/target/release/*"
|
||||
dependencies:
|
||||
deb:
|
||||
- "build-essential"
|
||||
- "curl"
|
||||
- "pkg-config"
|
||||
- "libssl-dev"
|
||||
rpm:
|
||||
- "gcc"
|
||||
- "gcc-c++"
|
||||
- "curl"
|
||||
- "openssl-devel"
|
||||
pacman:
|
||||
- "base-devel"
|
||||
- "curl"
|
||||
- "openssl"
|
||||
environment:
|
||||
RUSTUP_DIST_SERVER: "https://static.rust-lang.org"
|
||||
CARGO_HOME: "/usr/local/cargo"
|
||||
RUSTUP_HOME: "/usr/local/rustup"
|
||||
@@ -0,0 +1,24 @@
|
||||
name: "make"
|
||||
project_types:
|
||||
- "make"
|
||||
- "makefile"
|
||||
install_path: "/install"
|
||||
artifacts:
|
||||
- "/install/*"
|
||||
dependencies:
|
||||
deb:
|
||||
- "build-essential"
|
||||
- "curl"
|
||||
- "pkg-config"
|
||||
- "libssl-dev"
|
||||
rpm:
|
||||
- "gcc"
|
||||
- "gcc-c++"
|
||||
- "curl"
|
||||
- "openssl-devel"
|
||||
pacman:
|
||||
- "base-devel"
|
||||
- "curl"
|
||||
- "openssl"
|
||||
environment:
|
||||
NONE: "none"
|
||||
@@ -0,0 +1,62 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Cookbook {
|
||||
pub name: String,
|
||||
pub project_types: Vec<String>,
|
||||
pub install_path: String,
|
||||
pub artifacts: Vec<String>,
|
||||
pub dependencies: HashMap<String, Vec<String>>, // key: package_type (deb, rpm, etc.)
|
||||
pub environment: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CookbookError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("YAML error: {0}")]
|
||||
Yaml(#[from] serde_yaml::Error),
|
||||
#[error("Cookbook not found for project type: {0}")]
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
pub struct CookbookManager {
|
||||
cookbooks: HashMap<String, Cookbook>,
|
||||
}
|
||||
|
||||
impl CookbookManager {
|
||||
pub fn load_from_dir<P: AsRef<Path>>(dir: P) -> Result<Self, CookbookError> {
|
||||
let mut cookbooks = HashMap::new();
|
||||
|
||||
let entries = fs::read_dir(dir)?;
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().map_or(false, |ext| ext == "yaml" || ext == "yml") {
|
||||
if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) {
|
||||
if file_name.ends_with(".cookbook") {
|
||||
let content = fs::read_to_string(&path)?;
|
||||
let cookbook: Cookbook = serde_yaml::from_str(&content)?;
|
||||
|
||||
// Register all project types for this cookbook
|
||||
for project_type in &cookbook.project_types {
|
||||
cookbooks.insert(project_type.clone(), cookbook.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CookbookManager { cookbooks })
|
||||
}
|
||||
|
||||
pub fn get_cookbook(&self, project_type: &str) -> Result<&Cookbook, CookbookError> {
|
||||
self.cookbooks
|
||||
.get(project_type)
|
||||
.ok_or_else(|| CookbookError::NotFound(project_type.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
mod cookbook;
|
||||
mod llm;
|
||||
mod llm_output;
|
||||
|
||||
pub use cookbook::{Cookbook, CookbookManager, CookbookError};
|
||||
pub use llm::{LLMClient, ollama::OllamaClient, openai::OpenAIClient};
|
||||
pub use llm_output::LLMOutput;
|
||||
use serde::Serialize;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use anyhow::Result;
|
||||
use serde_yaml;
|
||||
|
||||
pub struct CICDLLMHelper {
|
||||
cookbook_manager: CookbookManager,
|
||||
}
|
||||
|
||||
impl CICDLLMHelper {
|
||||
pub fn new(cookbook_dir: &str) -> Result<Self, CookbookError> {
|
||||
let cookbook_manager = CookbookManager::load_from_dir(cookbook_dir)?;
|
||||
Ok(Self { cookbook_manager })
|
||||
}
|
||||
|
||||
pub async fn analyze_repository(
|
||||
&self,
|
||||
repository_path: &str,
|
||||
additional_files: &[String],
|
||||
llm_client: &dyn LLMClient,
|
||||
package_type: &str,
|
||||
) -> Result<AnalysisResult> {
|
||||
// Read README.md
|
||||
let readme_path = Path::new(repository_path).join("README.md");
|
||||
let readme_content = fs::read_to_string(&readme_path)
|
||||
.unwrap_or_else(|_| "No README.md found".to_string());
|
||||
|
||||
// Read additional files
|
||||
let mut file_contents = Vec::new();
|
||||
for file in additional_files {
|
||||
let file_path = Path::new(repository_path).join(file);
|
||||
if let Ok(content) = fs::read_to_string(&file_path) {
|
||||
file_contents.push(format!("File: {}\n{}\n", file, content));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare prompt for LLM
|
||||
let prompt = self.build_prompt(&readme_content, &file_contents, package_type);
|
||||
|
||||
// Get LLM response
|
||||
let llm_response = llm_client.generate(&prompt).await?;
|
||||
|
||||
println!("LLM Response: {}", llm_response);
|
||||
|
||||
// Parse LLM output
|
||||
let llm_output: LLMOutput = serde_yaml::from_str(&llm_response)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse LLM output: {}", e))?;
|
||||
|
||||
// Get cookbook for project type
|
||||
let cookbook = self.cookbook_manager.get_cookbook(&llm_output.project_type)?;
|
||||
|
||||
// Merge cookbook and LLM output
|
||||
let result = self.merge_outputs(&llm_output, cookbook, package_type);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn build_prompt(&self, readme_content: &str, file_contents: &[String], package_type: &str) -> String {
|
||||
format!(
|
||||
r#"Analyze the following repository files and determine the build configuration.
|
||||
|
||||
README.md:
|
||||
{}
|
||||
|
||||
Additional files:
|
||||
{}
|
||||
|
||||
Please provide the analysis in YAML format with the following structure:
|
||||
project_type: <string>
|
||||
dependencies: <list of package names for {package_type} packages>
|
||||
build_steps: <list of shell commands>
|
||||
artifacts: <list of artifact paths, use /install as base directory>
|
||||
environment: <key-value pairs for environment variables>
|
||||
other: <any other relevant information>
|
||||
|
||||
Focus on identifying:
|
||||
1. The project type (e.g., rust, python, nodejs, cmake, make, etc.)
|
||||
2. Dependencies needed for building (as {package_type} packages)
|
||||
3. Build steps as shell commands
|
||||
4. Artifact paths (output files, typically in /install)
|
||||
5. Environment variables needed for building
|
||||
|
||||
Output only valid YAML, no additional text and remove '```' in output(output as plain text)."#,
|
||||
readme_content,
|
||||
file_contents.join("\n"),
|
||||
package_type = package_type
|
||||
)
|
||||
}
|
||||
|
||||
fn merge_outputs(&self, llm_output: &LLMOutput, cookbook: &Cookbook, package_type: &str) -> AnalysisResult {
|
||||
// Merge dependencies
|
||||
let cookbook_deps = cookbook.dependencies.get(package_type)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut all_dependencies = cookbook_deps;
|
||||
all_dependencies.extend_from_slice(&llm_output.dependencies);
|
||||
all_dependencies.sort();
|
||||
all_dependencies.dedup();
|
||||
|
||||
// Merge environment variables
|
||||
let mut all_environment = cookbook.environment.clone();
|
||||
for (key, value) in &llm_output.environment {
|
||||
all_environment.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
// Merge artifacts
|
||||
let mut all_artifacts = cookbook.artifacts.clone();
|
||||
all_artifacts.extend_from_slice(&llm_output.artifacts);
|
||||
all_artifacts.sort();
|
||||
all_artifacts.dedup();
|
||||
|
||||
AnalysisResult {
|
||||
dependencies: all_dependencies,
|
||||
build_steps: llm_output.build_steps.clone(),
|
||||
artifacts: all_artifacts,
|
||||
environment: all_environment,
|
||||
project_type: llm_output.project_type.clone(),
|
||||
other: llm_output.other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_output_files(&self, result: &AnalysisResult, output_dir: &str) -> Result<()> {
|
||||
let output_path = Path::new(output_dir);
|
||||
|
||||
// Write prebake.tmpl.yaml
|
||||
let prebake_content = serde_yaml::to_string(&PrebakeTemplate {
|
||||
dependencies: &result.dependencies,
|
||||
environment: &result.environment,
|
||||
})?;
|
||||
fs::write(output_path.join("prebake.tmpl.yaml"), prebake_content)?;
|
||||
|
||||
// Write finalize.tmpl.yaml
|
||||
let finalize_content = serde_yaml::to_string(&FinalizeTemplate {
|
||||
artifacts: &result.artifacts,
|
||||
})?;
|
||||
fs::write(output_path.join("finalize.tmpl.yaml"), finalize_content)?;
|
||||
|
||||
// Write build.sh
|
||||
let build_script = result.build_steps.join("\n");
|
||||
fs::write(output_path.join("build.sh"), build_script)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnalysisResult {
|
||||
pub dependencies: Vec<String>,
|
||||
pub build_steps: Vec<String>,
|
||||
pub artifacts: Vec<String>,
|
||||
pub environment: HashMap<String, String>,
|
||||
pub project_type: String,
|
||||
pub other: HashMap<String, serde_yaml::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PrebakeTemplate<'a> {
|
||||
dependencies: &'a Vec<String>,
|
||||
environment: &'a HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FinalizeTemplate<'a> {
|
||||
artifacts: &'a Vec<String>,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use anyhow::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait LLMClient {
|
||||
async fn generate(&self, prompt: &str) -> Result<String>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mod client;
|
||||
|
||||
pub use client::LLMClient;
|
||||
|
||||
pub mod ollama;
|
||||
pub mod openai;
|
||||
@@ -0,0 +1,27 @@
|
||||
use async_trait::async_trait;
|
||||
use anyhow::Result;
|
||||
use ollama_rs::generation::completion::request::GenerationRequest;
|
||||
use ollama_rs::Ollama;
|
||||
|
||||
use super::LLMClient;
|
||||
|
||||
pub struct OllamaClient {
|
||||
client: Ollama,
|
||||
model: String,
|
||||
}
|
||||
|
||||
impl OllamaClient {
|
||||
pub fn new(url: String, model: String) -> Self {
|
||||
let client = Ollama::new(url, 11434);
|
||||
Self { client, model }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LLMClient for OllamaClient {
|
||||
async fn generate(&self, prompt: &str) -> Result<String> {
|
||||
let request = GenerationRequest::new(self.model.clone(), prompt.to_string());
|
||||
let response = self.client.generate(request).await?;
|
||||
Ok(response.response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use async_trait::async_trait;
|
||||
use anyhow::Result;
|
||||
use openai_api_rust::*;
|
||||
use openai_api_rust::chat::*;
|
||||
|
||||
use super::LLMClient;
|
||||
|
||||
pub struct OpenAIClient {
|
||||
client: OpenAI,
|
||||
model: String,
|
||||
}
|
||||
|
||||
impl OpenAIClient {
|
||||
pub fn new(api_key: String, base_url: Option<String>, model: String) -> Self {
|
||||
let auth = Auth::new(api_key.as_str());
|
||||
let client = if let Some(url) = base_url {
|
||||
OpenAI::new(auth, url.as_str())
|
||||
} else {
|
||||
OpenAI::new(auth, "https://api.openai.com/v1/")
|
||||
};
|
||||
Self { client, model }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LLMClient for OpenAIClient {
|
||||
async fn generate(&self, prompt: &str) -> Result<String> {
|
||||
let message = Message {
|
||||
role: Role::User,
|
||||
content: prompt.to_string(),
|
||||
};
|
||||
|
||||
let body = ChatBody {
|
||||
model: self.model.clone(),
|
||||
messages: vec![message],
|
||||
max_tokens: Some(2048),
|
||||
temperature: Some(0.1),
|
||||
top_p: Some(0.1),
|
||||
n: Some(1),
|
||||
stream: Some(false),
|
||||
stop: None,
|
||||
presence_penalty: None,
|
||||
frequency_penalty: None,
|
||||
logit_bias: None,
|
||||
user: None,
|
||||
};
|
||||
|
||||
let response = self.client.chat_completion_create(&body).unwrap();
|
||||
|
||||
if let Some(choice) = response.choices.first() {
|
||||
Ok(choice.message.as_ref().unwrap().content.clone())
|
||||
} else {
|
||||
anyhow::bail!("No response from OpenAI")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LLMOutput {
|
||||
pub project_type: String,
|
||||
pub dependencies: Vec<String>,
|
||||
pub build_steps: Vec<String>,
|
||||
pub artifacts: Vec<String>,
|
||||
pub environment: HashMap<String, String>,
|
||||
#[serde(flatten)]
|
||||
pub other: HashMap<String, serde_yaml::Value>,
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use clap::Parser;
|
||||
use log::error;
|
||||
use workshop_llm_detector::{CICDLLMHelper, LLMClient, OllamaClient, OpenAIClient};
|
||||
// use std::path::Path;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "workshop-llm-detector")]
|
||||
#[command(about = "LLM helper for analyzing repositories")]
|
||||
struct Cli {
|
||||
/// Repository path to analyze
|
||||
repository_path: String,
|
||||
|
||||
/// Additional files to read (comma separated)
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
files: Vec<String>,
|
||||
|
||||
/// LLM Provider
|
||||
#[arg(long, default_value = "openai")]
|
||||
provider: String,
|
||||
|
||||
/// Model to use
|
||||
#[arg(long)]
|
||||
model: String,
|
||||
|
||||
/// Model API URL
|
||||
#[arg(long)]
|
||||
model_url: Option<String>,
|
||||
|
||||
/// Package type for dependencies
|
||||
#[arg(long, default_value = "deb")]
|
||||
package_type: String,
|
||||
|
||||
/// Cookbook directory
|
||||
#[arg(long, default_value = "cookbook")]
|
||||
cookbook_dir: String,
|
||||
|
||||
/// Output directory
|
||||
#[arg(short, long, default_value = ".")]
|
||||
output_dir: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Cli::parse();
|
||||
|
||||
// Initialize cookbook manager
|
||||
let helper = CICDLLMHelper::new(&args.cookbook_dir)?;
|
||||
|
||||
// Initialize LLM client
|
||||
let llm_client: Box<dyn LLMClient> = match args.provider.as_str() {
|
||||
"openai" => {
|
||||
let api_key = std::env::var("OPENAI_API_KEY")
|
||||
.expect("OPENAI_API_KEY environment variable must be set");
|
||||
let base_url = args.model_url.clone();
|
||||
Box::new(OpenAIClient::new(api_key, base_url, args.model))
|
||||
}
|
||||
"ollama" => {
|
||||
let url = args.model_url.unwrap_or_else(|| "http://localhost:11434".to_string());
|
||||
Box::new(OllamaClient::new(url, args.model))
|
||||
}
|
||||
_ => {
|
||||
error!("Unsupported provider: {}", args.provider);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Analyze repository
|
||||
let result = helper.analyze_repository(
|
||||
&args.repository_path,
|
||||
&args.files,
|
||||
&*llm_client,
|
||||
&args.package_type,
|
||||
).await?;
|
||||
|
||||
println!("Analysis completed for project type: {}", result.project_type);
|
||||
println!("Found {} dependencies", result.dependencies.len());
|
||||
println!("Generated {} build steps", result.build_steps.len());
|
||||
println!("Identified {} artifacts", result.artifacts.len());
|
||||
|
||||
// Write output files
|
||||
helper.write_output_files(&result, &args.output_dir)?;
|
||||
|
||||
println!("Output files written to {}", args.output_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user