Refactored and documented the HTTP API
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||
use crate::llm::chat;
|
||||
use poem_openapi::{Object, OpenApi, payload::Json};
|
||||
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -9,6 +9,11 @@ struct ApiKeyPayload {
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Tags)]
|
||||
enum AnthropicTags {
|
||||
Anthropic,
|
||||
}
|
||||
|
||||
pub struct AnthropicApi {
|
||||
ctx: Arc<AppContext>,
|
||||
}
|
||||
@@ -19,8 +24,11 @@ impl AnthropicApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
#[OpenApi(tag = "AnthropicTags::Anthropic")]
|
||||
impl AnthropicApi {
|
||||
/// Check whether an Anthropic API key is stored.
|
||||
///
|
||||
/// Returns `true` if a non-empty key is present, otherwise `false`.
|
||||
#[oai(path = "/anthropic/key/exists", method = "get")]
|
||||
async fn get_anthropic_api_key_exists(&self) -> OpenApiResult<Json<bool>> {
|
||||
let exists =
|
||||
@@ -28,6 +36,9 @@ impl AnthropicApi {
|
||||
Ok(Json(exists))
|
||||
}
|
||||
|
||||
/// Store or update the Anthropic API key used for requests.
|
||||
///
|
||||
/// Returns `true` when the key is saved successfully.
|
||||
#[oai(path = "/anthropic/key", method = "post")]
|
||||
async fn set_anthropic_api_key(
|
||||
&self,
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||
use crate::llm::chat;
|
||||
use poem_openapi::{OpenApi, payload::Json};
|
||||
use poem_openapi::{OpenApi, Tags, payload::Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Tags)]
|
||||
enum ChatTags {
|
||||
Chat,
|
||||
}
|
||||
|
||||
pub struct ChatApi {
|
||||
pub ctx: Arc<AppContext>,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
#[OpenApi(tag = "ChatTags::Chat")]
|
||||
impl ChatApi {
|
||||
/// Cancel the currently running chat stream, if any.
|
||||
///
|
||||
/// Returns `true` once the cancellation signal is issued.
|
||||
#[oai(path = "/chat/cancel", method = "post")]
|
||||
async fn cancel_chat(&self) -> OpenApiResult<Json<bool>> {
|
||||
chat::cancel_chat(&self.ctx.state).map_err(bad_request)?;
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||
use crate::io::fs;
|
||||
use poem_openapi::{Object, OpenApi, payload::Json};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Deserialize, Object)]
|
||||
struct FilePathPayload {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Object)]
|
||||
struct WriteFilePayload {
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
pub struct FsApi {
|
||||
pub ctx: Arc<AppContext>,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
impl FsApi {
|
||||
#[oai(path = "/fs/read", method = "post")]
|
||||
async fn read_file(&self, payload: Json<FilePathPayload>) -> OpenApiResult<Json<String>> {
|
||||
let content = fs::read_file(payload.0.path, &self.ctx.state)
|
||||
.await
|
||||
.map_err(bad_request)?;
|
||||
Ok(Json(content))
|
||||
}
|
||||
|
||||
#[oai(path = "/fs/write", method = "post")]
|
||||
async fn write_file(&self, payload: Json<WriteFilePayload>) -> OpenApiResult<Json<bool>> {
|
||||
fs::write_file(payload.0.path, payload.0.content, &self.ctx.state)
|
||||
.await
|
||||
.map_err(bad_request)?;
|
||||
Ok(Json(true))
|
||||
}
|
||||
|
||||
#[oai(path = "/fs/list", method = "post")]
|
||||
async fn list_directory(
|
||||
&self,
|
||||
payload: Json<FilePathPayload>,
|
||||
) -> OpenApiResult<Json<Vec<fs::FileEntry>>> {
|
||||
let entries = fs::list_directory(payload.0.path, &self.ctx.state)
|
||||
.await
|
||||
.map_err(bad_request)?;
|
||||
Ok(Json(entries))
|
||||
}
|
||||
}
|
||||
94
server/src/http/io.rs
Normal file
94
server/src/http/io.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||
use crate::io::fs as io_fs;
|
||||
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Tags)]
|
||||
enum IoTags {
|
||||
Io,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Object)]
|
||||
struct FilePathPayload {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Object)]
|
||||
struct WriteFilePayload {
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Object)]
|
||||
struct SearchPayload {
|
||||
query: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Object)]
|
||||
struct ExecShellPayload {
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct IoApi {
|
||||
pub ctx: Arc<AppContext>,
|
||||
}
|
||||
|
||||
#[OpenApi(tag = "IoTags::Io")]
|
||||
impl IoApi {
|
||||
/// Read a file from the currently open project and return its contents.
|
||||
#[oai(path = "/io/fs/read", method = "post")]
|
||||
async fn read_file(&self, payload: Json<FilePathPayload>) -> OpenApiResult<Json<String>> {
|
||||
let content = io_fs::read_file(payload.0.path, &self.ctx.state)
|
||||
.await
|
||||
.map_err(bad_request)?;
|
||||
Ok(Json(content))
|
||||
}
|
||||
|
||||
/// Write a file to the currently open project, creating parent directories if needed.
|
||||
#[oai(path = "/io/fs/write", method = "post")]
|
||||
async fn write_file(&self, payload: Json<WriteFilePayload>) -> OpenApiResult<Json<bool>> {
|
||||
io_fs::write_file(payload.0.path, payload.0.content, &self.ctx.state)
|
||||
.await
|
||||
.map_err(bad_request)?;
|
||||
Ok(Json(true))
|
||||
}
|
||||
|
||||
/// List files and folders in a directory within the currently open project.
|
||||
#[oai(path = "/io/fs/list", method = "post")]
|
||||
async fn list_directory(
|
||||
&self,
|
||||
payload: Json<FilePathPayload>,
|
||||
) -> OpenApiResult<Json<Vec<io_fs::FileEntry>>> {
|
||||
let entries = io_fs::list_directory(payload.0.path, &self.ctx.state)
|
||||
.await
|
||||
.map_err(bad_request)?;
|
||||
Ok(Json(entries))
|
||||
}
|
||||
|
||||
/// Search the currently open project for files containing the provided query string.
|
||||
#[oai(path = "/io/search", method = "post")]
|
||||
async fn search_files(
|
||||
&self,
|
||||
payload: Json<SearchPayload>,
|
||||
) -> OpenApiResult<Json<Vec<crate::io::search::SearchResult>>> {
|
||||
let results = crate::io::search::search_files(payload.0.query, &self.ctx.state)
|
||||
.await
|
||||
.map_err(bad_request)?;
|
||||
Ok(Json(results))
|
||||
}
|
||||
|
||||
/// Execute an allowlisted shell command in the currently open project.
|
||||
#[oai(path = "/io/shell/exec", method = "post")]
|
||||
async fn exec_shell(
|
||||
&self,
|
||||
payload: Json<ExecShellPayload>,
|
||||
) -> OpenApiResult<Json<crate::io::shell::CommandOutput>> {
|
||||
let output =
|
||||
crate::io::shell::exec_shell(payload.0.command, payload.0.args, &self.ctx.state)
|
||||
.await
|
||||
.map_err(bad_request)?;
|
||||
Ok(Json(output))
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,22 @@ pub mod anthropic;
|
||||
pub mod assets;
|
||||
pub mod chat;
|
||||
pub mod context;
|
||||
pub mod fs;
|
||||
pub mod health;
|
||||
pub mod io;
|
||||
pub mod model;
|
||||
|
||||
pub mod project;
|
||||
pub mod search;
|
||||
pub mod shell;
|
||||
pub mod ws;
|
||||
|
||||
use anthropic::AnthropicApi;
|
||||
use chat::ChatApi;
|
||||
use context::AppContext;
|
||||
use fs::FsApi;
|
||||
use io::IoApi;
|
||||
use model::ModelApi;
|
||||
use poem::EndpointExt;
|
||||
use poem::{Route, get};
|
||||
use poem_openapi::OpenApiService;
|
||||
use project::ProjectApi;
|
||||
use search::SearchApi;
|
||||
use shell::ShellApi;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn build_routes(ctx: AppContext) -> impl poem::Endpoint {
|
||||
@@ -40,26 +36,17 @@ pub fn build_routes(ctx: AppContext) -> impl poem::Endpoint {
|
||||
.data(ctx_arc)
|
||||
}
|
||||
|
||||
type ApiTuple = (
|
||||
ProjectApi,
|
||||
ModelApi,
|
||||
AnthropicApi,
|
||||
FsApi,
|
||||
SearchApi,
|
||||
ShellApi,
|
||||
ChatApi,
|
||||
);
|
||||
type ApiTuple = (ProjectApi, ModelApi, AnthropicApi, IoApi, ChatApi);
|
||||
|
||||
type ApiService = OpenApiService<ApiTuple, ()>;
|
||||
|
||||
/// All HTTP methods are documented by OpenAPI at /docs
|
||||
pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
|
||||
let api = (
|
||||
ProjectApi { ctx: ctx.clone() },
|
||||
ModelApi { ctx: ctx.clone() },
|
||||
AnthropicApi::new(ctx.clone()),
|
||||
FsApi { ctx: ctx.clone() },
|
||||
SearchApi { ctx: ctx.clone() },
|
||||
ShellApi { ctx: ctx.clone() },
|
||||
IoApi { ctx: ctx.clone() },
|
||||
ChatApi { ctx: ctx.clone() },
|
||||
);
|
||||
|
||||
@@ -70,9 +57,7 @@ pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
|
||||
ProjectApi { ctx: ctx.clone() },
|
||||
ModelApi { ctx: ctx.clone() },
|
||||
AnthropicApi::new(ctx.clone()),
|
||||
FsApi { ctx: ctx.clone() },
|
||||
SearchApi { ctx: ctx.clone() },
|
||||
ShellApi { ctx: ctx.clone() },
|
||||
IoApi { ctx: ctx.clone() },
|
||||
ChatApi { ctx },
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||
use crate::io::fs;
|
||||
use crate::llm::chat;
|
||||
use poem_openapi::{Object, OpenApi, param::Query, payload::Json};
|
||||
use poem_openapi::{Object, OpenApi, Tags, param::Query, payload::Json};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Tags)]
|
||||
enum ModelTags {
|
||||
Model,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Object)]
|
||||
struct ModelPayload {
|
||||
model: String,
|
||||
@@ -14,20 +19,24 @@ pub struct ModelApi {
|
||||
pub ctx: Arc<AppContext>,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
#[OpenApi(tag = "ModelTags::Model")]
|
||||
impl ModelApi {
|
||||
/// Get the currently selected model preference, if any.
|
||||
#[oai(path = "/model", method = "get")]
|
||||
async fn get_model_preference(&self) -> OpenApiResult<Json<Option<String>>> {
|
||||
let result = fs::get_model_preference(self.ctx.store.as_ref()).map_err(bad_request)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// Persist the selected model preference.
|
||||
#[oai(path = "/model", method = "post")]
|
||||
async fn set_model_preference(&self, payload: Json<ModelPayload>) -> OpenApiResult<Json<bool>> {
|
||||
fs::set_model_preference(payload.0.model, self.ctx.store.as_ref()).map_err(bad_request)?;
|
||||
Ok(Json(true))
|
||||
}
|
||||
|
||||
/// Fetch available model names from an Ollama server.
|
||||
/// Optionally override the base URL via query string.
|
||||
#[oai(path = "/ollama/models", method = "get")]
|
||||
async fn get_ollama_models(
|
||||
&self,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||
use crate::io::fs;
|
||||
use poem_openapi::{Object, OpenApi, payload::Json};
|
||||
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Tags)]
|
||||
enum ProjectTags {
|
||||
Project,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Object)]
|
||||
struct PathPayload {
|
||||
path: String,
|
||||
@@ -13,8 +18,11 @@ pub struct ProjectApi {
|
||||
pub ctx: Arc<AppContext>,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
#[OpenApi(tag = "ProjectTags::Project")]
|
||||
impl ProjectApi {
|
||||
/// Get the currently open project path (if any).
|
||||
///
|
||||
/// Returns null when no project is open.
|
||||
#[oai(path = "/project", method = "get")]
|
||||
async fn get_current_project(&self) -> OpenApiResult<Json<Option<String>>> {
|
||||
let result = fs::get_current_project(&self.ctx.state, self.ctx.store.as_ref())
|
||||
@@ -22,6 +30,9 @@ impl ProjectApi {
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// Open a project and set it as the current project.
|
||||
///
|
||||
/// Persists the selected path for later sessions.
|
||||
#[oai(path = "/project", method = "post")]
|
||||
async fn open_project(&self, payload: Json<PathPayload>) -> OpenApiResult<Json<String>> {
|
||||
let confirmed = fs::open_project(payload.0.path, &self.ctx.state, self.ctx.store.as_ref())
|
||||
@@ -30,6 +41,7 @@ impl ProjectApi {
|
||||
Ok(Json(confirmed))
|
||||
}
|
||||
|
||||
/// Close the current project and clear the stored selection.
|
||||
#[oai(path = "/project", method = "delete")]
|
||||
async fn close_project(&self) -> OpenApiResult<Json<bool>> {
|
||||
fs::close_project(&self.ctx.state, self.ctx.store.as_ref()).map_err(bad_request)?;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||
use poem_openapi::{Object, OpenApi, payload::Json};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct SearchApi {
|
||||
pub ctx: Arc<AppContext>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Object)]
|
||||
struct SearchPayload {
|
||||
query: String,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
impl SearchApi {
|
||||
#[oai(path = "/fs/search", method = "post")]
|
||||
async fn search_files(
|
||||
&self,
|
||||
payload: Json<SearchPayload>,
|
||||
) -> OpenApiResult<Json<Vec<crate::io::search::SearchResult>>> {
|
||||
let ctx = self.ctx.clone();
|
||||
let results = crate::io::search::search_files(payload.0.query, &ctx.state)
|
||||
.await
|
||||
.map_err(bad_request)?;
|
||||
Ok(Json(results))
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request};
|
||||
use poem_openapi::{Object, OpenApi, payload::Json};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Deserialize, Object)]
|
||||
struct ExecShellPayload {
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct ShellApi {
|
||||
pub ctx: Arc<AppContext>,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
impl ShellApi {
|
||||
#[oai(path = "/shell/exec", method = "post")]
|
||||
async fn exec_shell(
|
||||
&self,
|
||||
payload: Json<ExecShellPayload>,
|
||||
) -> OpenApiResult<Json<crate::io::shell::CommandOutput>> {
|
||||
let output =
|
||||
crate::io::shell::exec_shell(payload.0.command, payload.0.args, &self.ctx.state)
|
||||
.await
|
||||
.map_err(bad_request)?;
|
||||
Ok(Json(output))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user