started gitea api impl #2
@@ -12,18 +12,8 @@
|
||||
"containerEnv": {
|
||||
"SHELL": "/bin/bash"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["rust-lang.rust-analyzer", "tamasfe.even-better-toml", "fill-labs.dependi"],
|
||||
"settings": {
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/herald,type=bind",
|
||||
"workspaceFolder": "/workspaces/herald",
|
||||
"runArgs": ["--userns=keep-id", "--security-opt", "label=disable"],
|
||||
"appPort": [3000]
|
||||
}
|
||||
|
||||
Generated
+258
-379
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -4,10 +4,14 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.13", features = ["json"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
tokio = { version = "1.52", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
tokio-util = "0.7"
|
||||
futures-util = "0.3"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
openrouter-rs = "0.10"
|
||||
dotenvy = "0.15"
|
||||
axum = "0.8"
|
||||
anyhow = "1.0"
|
||||
|
||||
+14
-4
@@ -1,9 +1,10 @@
|
||||
use axum::body::{Bytes, to_bytes};
|
||||
use axum::extract::{FromRef, FromRequest};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::extract::{FromRef, FromRequest, State};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::Value;
|
||||
use sha2::Sha256;
|
||||
use subtle::ConstantTimeEq;
|
||||
@@ -31,8 +32,17 @@ async fn root() -> &'static str {
|
||||
"Hi, i'm Herald :)"
|
||||
}
|
||||
|
||||
async fn webhook(WebhookExtract(wb): WebhookExtract) -> Result<Response, AppError> {
|
||||
Ok("lol".into_response())
|
||||
async fn webhook(
|
||||
State(app_state): State<AppState>,
|
||||
WebhookExtract(wb): WebhookExtract,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
app_state
|
||||
.bot_tx
|
||||
.send(wb)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
|
||||
Ok((StatusCode::CREATED, "Task started"))
|
||||
}
|
||||
|
||||
pub struct WebhookExtract(pub WebhookType);
|
||||
|
||||
+108
-4
@@ -1,13 +1,117 @@
|
||||
use crate::{env::EnvConfig, gitea::WebhookType};
|
||||
use serde::Deserialize;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
env::EnvConfig,
|
||||
gitea::{GiteaAPI, WebhookType},
|
||||
open_router::OpenRouterClient,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ReviewResult {
|
||||
pub reviews: Vec<ReviewItem>,
|
||||
pub comment: String,
|
||||
pub cost: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ReviewItem {
|
||||
pub filename: String,
|
||||
pub line: Option<u64>,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Map a filename to a markdown language identifier for syntax highlighting.
|
||||
fn lang_from_filename(filename: &str) -> &str {
|
||||
match std::path::Path::new(filename)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
{
|
||||
"rs" => "rust",
|
||||
"py" => "python",
|
||||
"js" | "mjs" => "javascript",
|
||||
"ts" => "typescript",
|
||||
"jsx" => "jsx",
|
||||
"tsx" => "tsx",
|
||||
"go" => "go",
|
||||
"java" => "java",
|
||||
"kt" | "kts" => "kotlin",
|
||||
"scala" => "scala",
|
||||
"c" | "h" => "c",
|
||||
"cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp",
|
||||
"rb" => "ruby",
|
||||
"php" => "php",
|
||||
"swift" => "swift",
|
||||
"sh" | "bash" | "zsh" => "bash",
|
||||
"sql" => "sql",
|
||||
"html" | "htm" => "html",
|
||||
"css" => "css",
|
||||
"scss" | "sass" => "scss",
|
||||
"json" => "json",
|
||||
"yaml" | "yml" => "yaml",
|
||||
"xml" => "xml",
|
||||
"toml" => "toml",
|
||||
"md" | "mdx" => "markdown",
|
||||
"dockerfile" | "Dockerfile" => "dockerfile",
|
||||
"Makefile" => "makefile",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Bot {
|
||||
config: EnvConfig,
|
||||
gitea_api: GiteaAPI,
|
||||
open_router_client: OpenRouterClient,
|
||||
http_client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl Bot {
|
||||
pub fn new(config: EnvConfig) -> Self {
|
||||
Self { config }
|
||||
pub fn new(config: EnvConfig) -> anyhow::Result<Self> {
|
||||
let gitea_timeout = config.gitea_timeout;
|
||||
let open_router_timeout = config.open_router_timeout;
|
||||
|
||||
Ok(Self {
|
||||
gitea_api: GiteaAPI::new(&config.gitea_url, &config.gitea_token, gitea_timeout)?,
|
||||
open_router_client: OpenRouterClient::new(
|
||||
&config.open_router_api_key,
|
||||
&config.open_router_model,
|
||||
open_router_timeout,
|
||||
)?,
|
||||
config,
|
||||
http_client: reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(gitea_timeout))
|
||||
.build()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn exec(&self, webhook: WebhookType) {}
|
||||
pub async fn start(
|
||||
&self,
|
||||
mut rx: tokio::sync::mpsc::Receiver<WebhookType>,
|
||||
) -> anyhow::Result<()> {
|
||||
while let Some(wb) = rx.recv().await {
|
||||
self.exec(wb).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn exec(&self, webhook: WebhookType) {
|
||||
let exec_result = match webhook {
|
||||
WebhookType::Review(review_payload) => crate::bot_actions::review::exec_review(
|
||||
&self.gitea_api,
|
||||
&self.open_router_client,
|
||||
&self.http_client,
|
||||
&self.config.open_router_model,
|
||||
review_payload,
|
||||
),
|
||||
}
|
||||
.await;
|
||||
|
||||
match exec_result {
|
||||
Ok(_) => println!("Task completed"),
|
||||
Err(err) => println!("{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod review;
|
||||
@@ -0,0 +1,201 @@
|
||||
use futures_util::stream::TryStreamExt;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio_util::io::StreamReader;
|
||||
|
||||
use crate::{
|
||||
bot::ReviewResult,
|
||||
consts::{BOT_PROCESS_MSG, MAX_DIFF_SIZE, REVIEW_PROMPT},
|
||||
errors::AppError,
|
||||
gitea::{GiteaAPI, ReviewPayload},
|
||||
open_router::OpenRouterClient,
|
||||
};
|
||||
|
||||
pub async fn exec_review(
|
||||
gitea_api: &GiteaAPI,
|
||||
open_router_client: &OpenRouterClient,
|
||||
http_client: &reqwest::Client,
|
||||
model: &str,
|
||||
review_payload: ReviewPayload,
|
||||
) -> Result<(), AppError> {
|
||||
let new_comment = gitea_api
|
||||
.comment(
|
||||
&BOT_PROCESS_MSG.replace("{model}", model),
|
||||
&review_payload.repository.full_name,
|
||||
review_payload.pull_request.number,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bot_result: Result<ReviewResult, anyhow::Error> = async {
|
||||
let git_diff =
|
||||
download_git_diff(&http_client, &review_payload.pull_request.diff_url).await?;
|
||||
|
||||
let diff_for_llm = format_diff_for_review(&git_diff);
|
||||
|
||||
let bot_request = REVIEW_PROMPT
|
||||
.replace("{subject}", &review_payload.pull_request.title)
|
||||
.replace("{comment}", &review_payload.comment.body)
|
||||
.replace("{diff}", &diff_for_llm);
|
||||
|
||||
let chat_result = open_router_client.chat(&bot_request).await?;
|
||||
let mut review_result = serde_json::from_str::<ReviewResult>(&chat_result.message)?;
|
||||
|
||||
review_result.cost = chat_result.cost;
|
||||
|
||||
gitea_api
|
||||
.post_pull_request_review(
|
||||
&review_result,
|
||||
&review_payload.repository.full_name,
|
||||
review_payload.pull_request.number,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(review_result)
|
||||
}
|
||||
.await;
|
||||
|
||||
let edit_msg = match bot_result {
|
||||
Ok(bot_result) => review_result_to_markdown(&bot_result),
|
||||
Err(e) => format!("Error while reviewing: {}", e),
|
||||
};
|
||||
|
||||
gitea_api
|
||||
.edit_comment(
|
||||
&edit_msg,
|
||||
&review_payload.repository.full_name,
|
||||
new_comment.id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn review_result_to_markdown(review_result: &ReviewResult) -> String {
|
||||
if review_result.reviews.is_empty() {
|
||||
return String::from("No issues found. ✅");
|
||||
}
|
||||
|
||||
let mut md = String::from("## Review Feedback\n\n");
|
||||
|
||||
md.push_str(&format!(
|
||||
"### {} issues found.\n\n",
|
||||
review_result.reviews.len()
|
||||
));
|
||||
|
||||
if !review_result.comment.is_empty() {
|
||||
md.push_str("\n---\n\n");
|
||||
md.push_str("### Summary\n\n");
|
||||
md.push_str(&review_result.comment);
|
||||
md.push('\n');
|
||||
}
|
||||
|
||||
if let Some(cost) = review_result.cost {
|
||||
md.push_str("\n---\n\n");
|
||||
md.push_str(&format!("### Cost: ${}", cost));
|
||||
md.push('\n');
|
||||
}
|
||||
|
||||
md
|
||||
}
|
||||
|
||||
async fn download_git_diff(http_client: &reqwest::Client, url: &str) -> anyhow::Result<String> {
|
||||
let response = http_client.get(url).send().await?;
|
||||
let stream = response.bytes_stream().map_err(std::io::Error::other);
|
||||
|
||||
let mut buf = Vec::with_capacity(MAX_DIFF_SIZE);
|
||||
StreamReader::new(stream)
|
||||
.take((MAX_DIFF_SIZE + 1) as u64)
|
||||
.read_to_end(&mut buf)
|
||||
.await?;
|
||||
|
||||
if buf.len() > MAX_DIFF_SIZE {
|
||||
anyhow::bail!("Git diff exceeds the maximum allowed size of 1 Mo");
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&buf).into_owned())
|
||||
}
|
||||
|
||||
fn format_diff_for_review(git_diff: &str) -> String {
|
||||
let mut output = String::new();
|
||||
let mut current_file: Option<&str> = None;
|
||||
let mut new_line: u64 = 0;
|
||||
|
||||
for line in git_diff.lines() {
|
||||
if let Some(rest) = line.strip_prefix("diff --git a/") {
|
||||
if let Some(end) = rest.find(' ') {
|
||||
current_file = Some(&rest[..end]);
|
||||
}
|
||||
new_line = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.starts_with("---") || line.starts_with("+++") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.starts_with("@@") && line.contains('+') {
|
||||
if let Some(start) = parse_hunk_new_start(line) {
|
||||
new_line = start;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(filename) = current_file else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if line.starts_with(' ') {
|
||||
new_line += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(code) = line.strip_prefix('+') {
|
||||
use std::fmt::Write;
|
||||
let _ = writeln!(output, "{filename}:{new_line}:{code}");
|
||||
new_line += 1;
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn parse_hunk_new_start(hunk_header: &str) -> Option<u64> {
|
||||
let plus_part = hunk_header.split('+').nth(1)?;
|
||||
let num_str = plus_part.split(|c: char| !c.is_ascii_digit()).next()?;
|
||||
num_str.parse::<u64>().ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_format_diff_for_review() {
|
||||
let diff = concat!(
|
||||
"diff --git a/src/foo.rs b/src/foo.rs\n",
|
||||
"--- a/src/foo.rs\n",
|
||||
"+++ b/src/foo.rs\n",
|
||||
"@@ -1,3 +1,6 @@\n",
|
||||
" fn main() {\n",
|
||||
"+ let x = 1;\n",
|
||||
" println!(\"hello\");\n",
|
||||
"+ let y = 2;\n",
|
||||
"+ let z = 3;\n",
|
||||
" }\n",
|
||||
"diff --git a/src/bar.rs b/src/bar.rs\n",
|
||||
"--- a/src/bar.rs\n",
|
||||
"+++ b/src/bar.rs\n",
|
||||
"@@ -10,4 +10,6 @@\n",
|
||||
" old context\n",
|
||||
"+ let a = 10;\n",
|
||||
" more context\n",
|
||||
"+ let b = 20;\n",
|
||||
);
|
||||
|
||||
let result = format_diff_for_review(diff);
|
||||
let expected = concat!(
|
||||
"src/foo.rs:2: let x = 1;\n",
|
||||
"src/foo.rs:4: let y = 2;\n",
|
||||
"src/foo.rs:5: let z = 3;\n",
|
||||
"src/bar.rs:11: let a = 10;\n",
|
||||
"src/bar.rs:13: let b = 20;\n",
|
||||
);
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
@@ -1,3 +1,44 @@
|
||||
pub const GITEA_SIG_HEADER_NAME: &str = "x-gitea-signature";
|
||||
pub const GITEA_EVENT_TYPE_HEADER_NAME: &str = "x-gitea-event-type";
|
||||
|
||||
pub const MAX_WEBHOOK_BODY_SIZE: usize = 1024 * 1024; // 1 MiB
|
||||
pub const MAX_DIFF_SIZE: usize = 1024 * 1024; // 1 MiB
|
||||
|
||||
pub const BOT_PROCESS_MSG: &str = "
|
||||
Review in progress with the model \"{model}\"...
|
||||
";
|
||||
|
||||
pub const REVIEW_PROMPT: &str = "
|
||||
You are a senior software engineer reviewing code changes.
|
||||
|
||||
Check good practices and code quality.
|
||||
|
||||
This is the pull request subject: \"{subject}\"
|
||||
|
||||
This is the user comment: \"{comment}\"
|
||||
|
||||
The code changes (only added lines, with line numbers):
|
||||
|
||||
{diff}
|
||||
|
||||
Please review the code changes and provide feedback.
|
||||
|
||||
IMPORTANT: the `line` field must be the line number shown before each line.
|
||||
The provided code has the format: `filename:line:code`
|
||||
|
||||
Return your feedback, in french, with only this json format, reviews must contain each review
|
||||
All fields are mandatory.
|
||||
(filename field must contain the full path with extension) and comment must contain a final summary:
|
||||
|
||||
{
|
||||
\"reviews\": [
|
||||
{
|
||||
\"filename\": \"\",
|
||||
\"line\": ,
|
||||
\"code\": \"\",
|
||||
\"message\": \"\"
|
||||
}
|
||||
],
|
||||
\"comment\": \"\"
|
||||
}
|
||||
";
|
||||
|
||||
+15
@@ -6,7 +6,12 @@ pub struct EnvConfig {
|
||||
pub http_port: u16,
|
||||
pub webhook_secret: String,
|
||||
pub open_router_api_key: String,
|
||||
pub open_router_model: String,
|
||||
pub open_router_timeout: u64,
|
||||
pub bot_name: String,
|
||||
pub gitea_url: String,
|
||||
pub gitea_token: String,
|
||||
pub gitea_timeout: u64,
|
||||
}
|
||||
|
||||
pub fn load_config() -> anyhow::Result<EnvConfig> {
|
||||
@@ -16,12 +21,22 @@ pub fn load_config() -> anyhow::Result<EnvConfig> {
|
||||
let bot_name = try_get_env("BOT_NAME")?;
|
||||
let webhook_secret = try_get_env("WEBHOOK_SIG_HEADER_SECRET")?;
|
||||
let open_router_api_key = try_get_env("OPEN_ROUTER_API_KEY")?;
|
||||
let open_router_model = try_get_env("OPEN_ROUTER_MODEL")?;
|
||||
let open_router_timeout = try_get_env("OPEN_ROUTER_TIMEOUT")?.parse()?;
|
||||
let gitea_url = try_get_env("GITEA_URL")?;
|
||||
let gitea_token = try_get_env("GITEA_TOKEN")?;
|
||||
let gitea_timeout = try_get_env("GITEA_TIMEOUT")?.parse()?;
|
||||
|
||||
Ok(EnvConfig {
|
||||
http_port,
|
||||
webhook_secret,
|
||||
bot_name,
|
||||
open_router_api_key,
|
||||
open_router_model,
|
||||
open_router_timeout,
|
||||
gitea_url,
|
||||
gitea_token,
|
||||
gitea_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+139
-3
@@ -1,7 +1,135 @@
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::errors::AppError;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{
|
||||
bot::{ReviewItem, ReviewResult},
|
||||
errors::AppError,
|
||||
};
|
||||
|
||||
pub struct GiteaAPI {
|
||||
base_url: String,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl GiteaAPI {
|
||||
pub fn new(base_url: &str, token: &str, timeout: u64) -> anyhow::Result<Self> {
|
||||
let mut default_headers = reqwest::header::HeaderMap::new();
|
||||
default_headers.insert(
|
||||
reqwest::header::HeaderName::from_static("authorization"),
|
||||
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", token))?,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
base_url: String::from(base_url),
|
||||
client: reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(timeout))
|
||||
.default_headers(default_headers)
|
||||
.build()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn comment(
|
||||
&self,
|
||||
body: &str,
|
||||
full_name: &str,
|
||||
index: u64,
|
||||
) -> anyhow::Result<Comment> {
|
||||
let url = format!(
|
||||
"{}/api/v1/repos/{}/issues/{}/comments",
|
||||
self.base_url, full_name, index
|
||||
);
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(url)
|
||||
.json(&json!({
|
||||
"body": body
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !res.status().is_success() {
|
||||
return Err(anyhow::anyhow!("Failed to comment: {}", res.status()));
|
||||
}
|
||||
|
||||
res.json::<Comment>().await.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
pub async fn edit_comment(
|
||||
&self,
|
||||
body: &str,
|
||||
full_name: &str,
|
||||
comment_id: u64,
|
||||
) -> anyhow::Result<()> {
|
||||
let url = format!(
|
||||
"{}/api/v1/repos/{}/issues/comments/{}",
|
||||
self.base_url, full_name, comment_id
|
||||
);
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.patch(url)
|
||||
.json(&json!({
|
||||
"body": body
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !res.status().is_success() {
|
||||
return Err(anyhow::anyhow!("Failed to comment: {}", res.status()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn post_pull_request_review(
|
||||
&self,
|
||||
review_result: &ReviewResult,
|
||||
full_name: &str,
|
||||
index: u64,
|
||||
) -> anyhow::Result<()> {
|
||||
let url = format!(
|
||||
"{}/api/v1/repos/{}/pulls/{}/reviews",
|
||||
self.base_url, full_name, index
|
||||
);
|
||||
|
||||
let comments = &&review_result
|
||||
.reviews
|
||||
.iter()
|
||||
.filter(|r| r.line.is_some())
|
||||
.map(|r| {
|
||||
let path = r.filename.clone();
|
||||
let line = r.line.unwrap_or(0);
|
||||
let body = r.message.clone();
|
||||
|
||||
json!({
|
||||
"path": path,
|
||||
"new_position": line,
|
||||
"body": body
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(url)
|
||||
.json(&json!({
|
||||
"event": "COMMENT",
|
||||
"body": review_result.comment,
|
||||
"comments": comments
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !res.status().is_success() {
|
||||
return Err(anyhow::anyhow!("Failed to post review: {}", res.status()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WebhookType {
|
||||
@@ -12,6 +140,7 @@ pub enum WebhookType {
|
||||
pub struct ReviewPayload {
|
||||
pub action: String,
|
||||
pub pull_request: PullRequest,
|
||||
pub repository: Repository,
|
||||
pub comment: Comment,
|
||||
}
|
||||
|
||||
@@ -19,6 +148,8 @@ pub struct ReviewPayload {
|
||||
pub struct PullRequest {
|
||||
pub id: u64,
|
||||
pub diff_url: String,
|
||||
pub number: u64,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -33,6 +164,11 @@ pub struct User {
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Repository {
|
||||
pub full_name: String,
|
||||
}
|
||||
|
||||
impl WebhookType {
|
||||
pub fn from_event(event: &str, bot_name: &str, json: Value) -> Result<Self, AppError> {
|
||||
let wb = match event {
|
||||
|
||||
+10
-10
@@ -1,25 +1,25 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{bot::Bot, state::AppState};
|
||||
use crate::{bot::Bot, gitea::WebhookType, state::AppState};
|
||||
|
||||
mod api;
|
||||
mod bot;
|
||||
mod bot_actions;
|
||||
mod consts;
|
||||
mod env;
|
||||
mod errors;
|
||||
mod gitea;
|
||||
mod open_router;
|
||||
mod state;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let config = env::load_config()?;
|
||||
let bot = Bot::new(config.clone())?;
|
||||
|
||||
let app_state = AppState {
|
||||
bot: Arc::new(Mutex::new(Bot::new(config.clone()))),
|
||||
config: config,
|
||||
};
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<WebhookType>(1);
|
||||
|
||||
api::start(app_state).await
|
||||
let app_state = AppState { bot_tx: tx, config };
|
||||
|
||||
tokio::try_join!(bot.start(rx), api::start(app_state))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use openrouter_rs::{Message, api::chat::ChatCompletionRequest};
|
||||
|
||||
pub struct ChatResult {
|
||||
pub message: String,
|
||||
pub cost: Option<f64>,
|
||||
}
|
||||
|
||||
pub struct OpenRouterClient {
|
||||
client: openrouter_rs::OpenRouterClient,
|
||||
model: String,
|
||||
}
|
||||
|
||||
impl OpenRouterClient {
|
||||
pub fn new(token: &str, model: &str, timeout: u64) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
client: openrouter_rs::OpenRouterClient::builder()
|
||||
.api_key(token)
|
||||
.http_client(
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(timeout))
|
||||
.build()?,
|
||||
)
|
||||
.build()?,
|
||||
model: String::from(model),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn chat(&self, msg: &str) -> anyhow::Result<ChatResult> {
|
||||
let request = ChatCompletionRequest::builder()
|
||||
.model(&self.model)
|
||||
.enable_reasoning()
|
||||
.messages(vec![Message::new(openrouter_rs::types::Role::User, msg)])
|
||||
.build()?;
|
||||
|
||||
let response = self.client.chat().create(&request).await?;
|
||||
|
||||
Ok(ChatResult {
|
||||
message: response.choices[0]
|
||||
.content()
|
||||
.map(|msg| String::from(msg))
|
||||
.ok_or(anyhow::anyhow!("No content"))?,
|
||||
cost: response.usage.and_then(|u| u.cost),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -1,10 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{bot::Bot, env::EnvConfig};
|
||||
use crate::{env::EnvConfig, gitea::WebhookType};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub bot: Arc<Mutex<Bot>>,
|
||||
pub bot_tx: tokio::sync::mpsc::Sender<WebhookType>,
|
||||
pub config: EnvConfig,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user