Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,13 @@ npm run dev # or bun run dev

The UI will be available at `http://localhost:3000`.

### Build Static Docs

To generate static files for deployment:
```bash
npx vuepress build
```
The output will be in `docs/.vuepress/dist`.

## Contributing

Expand Down
1 change: 1 addition & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"
anyhow = "1.0.95"
argon2 = { version = "0.5.3", features = ["std"] }
# async-openai = "0.28.2"
regex = "1"

# temporary until async-openai fixes their latest release
async-openai = { git = "https://github.com/64bit/async-openai" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- Add prompt_directory table
CREATE TABLE prompt_directory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
parent_id INTEGER REFERENCES prompt_directory(id) ON DELETE SET NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Add prompt_component table
CREATE TABLE prompt_component (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
content TEXT NOT NULL,
description TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Add directory_id to prompt table
ALTER TABLE prompt ADD COLUMN directory_id INTEGER REFERENCES prompt_directory(id) ON DELETE SET NULL;
62 changes: 62 additions & 0 deletions backend/src/controllers/prompt_components.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use axum::{
extract::{Path, State},
Json,
};
use crate::{AppState, AppError};
use crate::db::models::PromptComponent;

pub async fn create_component(
State(state): State<AppState>,
Json(payload): Json<PromptComponent>,
) -> Result<Json<i64>, AppError> {
let id = state.db.prompt_component.create_component(
&payload.name,
&payload.content,
payload.description.as_deref(),
).await?;
Ok(Json(id))
}

pub async fn get_component(
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<Json<PromptComponent>, AppError> {
let component = state.db.prompt_component.get_component(id).await?;
Ok(Json(component))
}

pub async fn list_components(
State(state): State<AppState>,
) -> Result<Json<Vec<PromptComponent>>, AppError> {
let components = state.db.prompt_component.list_components().await?;
Ok(Json(components))
}

pub async fn update_component(
Path(id): Path<i64>,
State(state): State<AppState>,
Json(payload): Json<PromptComponent>,
) -> Result<Json<PromptComponent>, AppError> {
let updated = state.db.prompt_component.update_component(
id,
&payload.name,
&payload.content,
payload.description.as_deref(),
).await?;
if !updated {
return Err(AppError::NotFound("Component not found".into()));
}
let component = state.db.prompt_component.get_component(id).await?;
Ok(Json(component))
}

pub async fn delete_component(
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<(), AppError> {
let deleted = state.db.prompt_component.delete_component(id).await?;
if !deleted {
return Err(AppError::NotFound("Component not found".into()));
}
Ok(())
}
50 changes: 50 additions & 0 deletions backend/src/controllers/prompt_directories.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// --- Prompt Directory API ---
use crate::db::models::{PromptDirectory, PromptComponent};
use axum::extract::Query;
use serde::Deserialize;

#[derive(Deserialize)]
pub struct DirectoryQuery {
pub parent_id: Option<i64>,
}

pub async fn create_directory(
State(state): State<AppState>,
Json(payload): Json<PromptDirectory>,
) -> Result<Json<i64>, AppError> {
let id = state.db.prompt.create_directory(&payload.name, payload.parent_id).await?;
Ok(Json(id))
}

pub async fn get_directory(
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<Json<Option<PromptDirectory>>, AppError> {
let dir = state.db.prompt.get_directory(id).await?;
Ok(Json(dir))
}

pub async fn list_directories(
State(state): State<AppState>,
Query(query): Query<DirectoryQuery>,
) -> Result<Json<Vec<PromptDirectory>>, AppError> {
let dirs = state.db.prompt.list_directories(query.parent_id).await?;
Ok(Json(dirs))
}

pub async fn update_directory(
Path(id): Path<i64>,
State(state): State<AppState>,
Json(payload): Json<PromptDirectory>,
) -> Result<Json<bool>, AppError> {
let updated = state.db.prompt.update_directory(id, &payload.name, payload.parent_id).await?;
Ok(Json(updated))
}

pub async fn delete_directory(
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<Json<bool>, AppError> {
let deleted = state.db.prompt.delete_directory(id).await?;
Ok(Json(deleted))
}
8 changes: 7 additions & 1 deletion backend/src/db/logs.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
// IMPORTANT: SQLx query macros need a live database connection at compile time.
// Make sure to set DATABASE_URL before building.
// For example:
// On bash: export DATABASE_URL="sqlite://C:/Users/kunya/PycharmProjects/llmkit/backend/llmkit.db"
// On PowerShell: $env:DATABASE_URL="sqlite://C:/Users/kunya/PycharmProjects/llmkit/backend/llmkit.db"
// Alternatively, run `cargo sqlx prepare` to generate an offline query cache.

use anyhow::Result;
use crate::db::types::log::{LogRow, LogRowModel};

Expand Down Expand Up @@ -226,4 +233,3 @@ impl LogRepository {
Ok(log)
}
}

35 changes: 35 additions & 0 deletions backend/src/db/models.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use anyhow::Result;
use crate::db::types::models::ModelProviderRow;

// Removed: use async_openai::types::Prompt; // no longer needed

#[derive(Clone, Debug)]
pub struct ModelRepository {
pool: sqlx::SqlitePool,
Expand Down Expand Up @@ -160,3 +162,36 @@ impl ModelRepository {
Ok(model)
}
}

// --- Prompt Directory ---
#[derive(Debug, Clone)]
pub struct PromptDirectory {
pub id: i64,
pub name: String,
pub parent_id: Option<i64>,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}

// --- Prompt Component ---
#[derive(Debug, Clone)]
pub struct PromptComponent {
pub id: i64,
pub name: String,
pub content: String,
pub description: Option<String>,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}

// --- Prompt ---
// Rename the local Prompt type to DbPrompt so that you can derive traits
#[derive(Debug, Clone)]
pub struct DbPrompt {
pub id: i64,
pub key: String,
pub current_prompt_version_id: Option<i64>,
pub directory_id: Option<i64>,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}
109 changes: 109 additions & 0 deletions backend/src/db/prompt_components.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use crate::db::models::PromptComponent;
use regex::Regex;

// --- Prompt Component CRUD ---
pub async fn create_component(&self, name: &str, content: &str, description: Option<&str>) -> Result<i64> {
let rec = sqlx::query!(
r#"INSERT INTO prompt_component (name, content, description) VALUES (?, ?, ?)"#,
name, content, description
)
.execute(&self.pool)
.await?;
Ok(rec.last_insert_rowid())
}

pub async fn get_component(&self, id: i64) -> Result<Option<PromptComponent>> {
let rec = sqlx::query_as!(PromptComponent,
r#"SELECT id, name, content, description, created_at, updated_at FROM prompt_component WHERE id = ?"#,
id
)
.fetch_optional(&self.pool)
.await?;
Ok(rec)
}

pub async fn list_components(&self) -> Result<Vec<PromptComponent>> {
let recs = sqlx::query_as!(PromptComponent,
r#"SELECT id, name, content, description, created_at, updated_at FROM prompt_component ORDER BY created_at DESC"#
)
.fetch_all(&self.pool)
.await?;
Ok(recs)
}

pub async fn update_component(&self, id: i64, name: &str, content: &str, description: Option<&str>) -> Result<bool> {
let rows = sqlx::query!(
r#"UPDATE prompt_component SET name = ?, content = ?, description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#,
name, content, description, id
)
.execute(&self.pool)
.await?
.rows_affected();
Ok(rows > 0)
}

pub async fn delete_component(&self, id: i64) -> Result<bool> {
let rows = sqlx::query!(
r#"DELETE FROM prompt_component WHERE id = ?"#,
id
)
.execute(&self.pool)
.await?
.rows_affected();
Ok(rows > 0)
}

// Utility: Recursively resolve {{component:component_name}} in prompt content
pub async fn resolve_components_in_text(&self, text: &str) -> Result<String> {
let re = Regex::new(r"\{\{component:([a-zA-Z0-9_\- ]+)}}")?;
let mut resolved = text.to_string();
let mut changed = true;
while changed {
changed = false;
let mut new_text = resolved.clone();
for cap in re.captures_iter(&resolved) {
let comp_name = &cap[1];
let comp = sqlx::query!(
r#"SELECT content FROM prompt_component WHERE name = ? LIMIT 1"#,
comp_name
)
.fetch_optional(&self.pool)
.await?;
if let Some(row) = comp {
let comp_content = row.content;
new_text = new_text.replace(&cap[0], &comp_content);
changed = true;
}
}
resolved = new_text;
}
Ok(resolved)
}

// Wrap get_prompt to resolve components in system/user fields
pub async fn get_prompt_with_components(&self, id: i64) -> Result<PromptRowWithModel> {
let mut prompt = self.get_prompt(id).await?;
if let Some(system) = &prompt.system {
prompt.system = Some(self.resolve_components_in_text(system).await?);
}
if let Some(user) = &prompt.user {
prompt.user = Some(self.resolve_components_in_text(user).await?);
}
Ok(prompt)
}
}

fn generate_diff(text1: &str, text2: &str) -> String {
let mut diff_string = String::new();
let differences = lines(text1, text2);

for difference in differences {
match difference {
DiffResult::Left(l) => diff_string.push_str(&format!("-{}\n", l)),
DiffResult::Right(r) => diff_string.push_str(&format!("+{}\n", r)),
_ => {}
}
}

diff_string
}
57 changes: 57 additions & 0 deletions backend/src/db/prompt_directories.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::db::models::PromptDirectory;

// --- Prompt Directory CRUD ---
pub async fn create_directory(&self, name: &str, parent_id: Option<i64>) -> Result<i64> {
let rec = sqlx::query!(
r#"INSERT INTO prompt_directory (name, parent_id) VALUES (?, ?)"#,
name,
parent_id
)
.execute(&self.pool)
.await?;
Ok(rec.last_insert_rowid())
}

pub async fn get_directory(&self, id: i64) -> Result<Option<PromptDirectory>> {
let rec = sqlx::query_as!(PromptDirectory,
r#"SELECT id, name, parent_id, created_at, updated_at FROM prompt_directory WHERE id = ?"#,
id
)
.fetch_optional(&self.pool)
.await?;
Ok(rec)
}

pub async fn list_directories(&self, parent_id: Option<i64>) -> Result<Vec<PromptDirectory>> {
let recs = sqlx::query_as!(PromptDirectory,
r#"SELECT id, name, parent_id, created_at, updated_at FROM prompt_directory WHERE parent_id IS ?"#,
parent_id
)
.fetch_all(&self.pool)
.await?;
Ok(recs)
}

pub async fn update_directory(&self, id: i64, name: &str, parent_id: Option<i64>) -> Result<bool> {
let rows = sqlx::query!(
r#"UPDATE prompt_directory SET name = ?, parent_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#,
name, parent_id, id
)
.execute(&self.pool)
.await?
.rows_affected();
Ok(rows > 0)
}

pub async fn delete_directory(&self, id: i64) -> Result<bool> {
let rows = sqlx::query!(
r#"DELETE FROM prompt_directory WHERE id = ?"#,
id
)
.execute(&self.pool)
.await?
.rows_affected();
Ok(rows > 0)
}

// All table names below should be prompt_directory (singular)
Loading