Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Lint codebase
on:
push:
branches:
- main
pull_request:

# Make sure CI fails on all warnings, including Clippy lints
env:
RUSTFLAGS: "-Dwarnings"

jobs:
clippy_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Run Clippy
run: cargo clippy --all-targets --all-features
15 changes: 15 additions & 0 deletions src/error/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// This module contains the custom errors for this project.
use std::{env::VarError, string::FromUtf8Error};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum LLMError {
#[error("conversion error: {0}")]
BytesConversion(#[from] FromUtf8Error),
#[error("cli not found: {0}")]
CLINotFound(String),
#[error("missing env variable: {0}")]
Env(#[from] VarError),
#[error("failed to prompt: {0}")]
Prompt(String),
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod error;
pub mod llm;
27 changes: 27 additions & 0 deletions src/llm/anthropic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use crate::error::LLMError;

use super::{Model, ModelFactory};

use std::env::var;

pub struct Anthropic {
api_key: String,
}

impl ModelFactory for Anthropic {
fn init() -> Result<Self, LLMError> {
let api_key = var("ANTHROPIC_API_KEY")?;

Ok(Self { api_key })
}
}

impl Model for Anthropic {
fn get_name(&self) -> String {
"Anthropic API".into()
}

fn prompt(&self, _: &str) -> Result<String, LLMError> {
unimplemented!("anthropic api")
}
}
36 changes: 36 additions & 0 deletions src/llm/claude.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::process::Command;

use crate::{
error::LLMError,
llm::{Model, ModelFactory},
};

const CLAUDE_CLI_NAME: &str = "claude";

pub struct Claude {}

impl ModelFactory for Claude {
fn init() -> Result<Self, LLMError> {
Command::new(CLAUDE_CLI_NAME)
.arg("-h")
.output()
.map_err(|_| LLMError::CLINotFound(CLAUDE_CLI_NAME.into()))?;

Ok(Self {})
}
}

impl Model for Claude {
fn get_name(&self) -> String {
"Claude CLI".into()
}

fn prompt(&self, input: &str) -> Result<String, LLMError> {
let out = Command::new(CLAUDE_CLI_NAME)
.args([input, "-p"])
.output()
.map_err(|e| LLMError::Prompt(e.to_string()))?;

Ok(String::from_utf8(out.stdout)?)
}
}
36 changes: 36 additions & 0 deletions src/llm/cursor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crate::{
error::LLMError,
llm::{Model, ModelFactory},
};

use std::process::Command;

const CURSOR_CLI_NAME: &str = "cursor-agent";

pub struct CursorCLI {}

impl ModelFactory for CursorCLI {
fn init() -> Result<Self, LLMError> {
Command::new(CURSOR_CLI_NAME)
.arg("-h")
.output()
.map_err(|_| LLMError::CLINotFound(CURSOR_CLI_NAME.into()))?;

Ok(Self {})
}
}

impl Model for CursorCLI {
fn get_name(&self) -> String {
"Cursor CLI".into()
}

fn prompt(&self, input: &str) -> Result<String, LLMError> {
let out = Command::new(CURSOR_CLI_NAME)
.args([input, "-p"])
.output()
.map_err(|e| LLMError::Prompt(e.to_string()))?;

Ok(String::from_utf8(out.stdout)?)
}
}
58 changes: 58 additions & 0 deletions src/llm/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/// The `llm` module contains the required logic
/// to interact with a generalized selection of
/// supported models.
mod anthropic;
mod claude;
mod cursor;
mod openai;

use crate::{
error::LLMError,
llm::{anthropic::Anthropic, claude::Claude, cursor::CursorCLI, openai::OpenAI},
};

/// Defines the expected interface for the initialization
/// of the different supported models.
///
/// Note: It's required to separate this from the actual model
/// trait, that defines the interface for interaction with the
/// LLMs. This is because we store the actual `Model` in a boxed
/// vector.
///
/// To allow this, the instances of `Model` have to be `dyn`-compatible,
/// which requires the trait not to be `Sized` as it is described here:
/// https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility.
pub trait ModelFactory: Model + Sized {
fn init() -> Result<Self, LLMError>;
}

/// Defines the required functionality
/// to interact with a language model.
pub trait Model {
fn get_name(&self) -> String;
fn prompt(&self, input: &str) -> Result<String, LLMError>;
}

/// Returns the available models in the current
/// system context.
pub fn get_available_models() -> Result<Vec<Box<dyn Model>>, LLMError> {
let mut models: Vec<Box<dyn Model>> = vec![];

if let Ok(m) = Anthropic::init() {
models.push(Box::new(m))
}

if let Ok(m) = Claude::init() {
models.push(Box::new(m))
}

if let Ok(m) = CursorCLI::init() {
models.push(Box::new(m))
}

if let Ok(m) = OpenAI::init() {
models.push(Box::new(m))
}

Ok(models)
}
27 changes: 27 additions & 0 deletions src/llm/openai.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use std::env::var;

use crate::{
error::LLMError,
llm::{Model, ModelFactory},
};

pub struct OpenAI {
api_key: String,
}

impl ModelFactory for OpenAI {
fn init() -> Result<Self, LLMError> {
let api_key = var("OPENAI_API_KEY")?;
Ok(Self { api_key })
}
}

impl Model for OpenAI {
fn get_name(&self) -> String {
"OpenAI API".into()
}

fn prompt(&self, _: &str) -> Result<String, LLMError> {
unimplemented!("open ai api")
}
}
12 changes: 12 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use parrot::llm::get_available_models;

fn main() {
let available_models = get_available_models().expect("failed to get models");
available_models.iter().for_each(|m| {
let out = m
.prompt("say hello to my friends")
.expect("failed to prompt");

println!("{} - {}", m.get_name(), out);
})
}
Loading