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
14 changes: 11 additions & 3 deletions Cargo.lock

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

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ documentation = "https://cydonia.docs.rs"
keywords = ["llm", "agent", "ai"]

[workspace.dependencies]
model = { path = "legacy/model", package = "cydonia-model" }
candle = { path = "crates/candle", package = "cydonia-candle" }
ucore = { path = "crates/core", package = "ullm-core" }
deepseek = { path = "llm/deepseek", package = "ullm-deepseek" }

model = { path = "legacy/model", package = "cydonia-model" }
ullm = { path = "crates/ullm" }
ucore = { path = "crates/core", package = "ullm-core" }
ucli = { path = "crates/cli", package = "ullm-cli" }

# crates.io
anyhow = "1"
Expand Down
22 changes: 22 additions & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "ullm-cli"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
documentation.workspace = true
keywords.workspace = true

[dependencies]
ullm.workspace = true

# crates-io dependencies
anyhow.workspace = true
clap.workspace = true
dirs.workspace = true
futures-util.workspace = true
serde.workspace = true
toml.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
2 changes: 1 addition & 1 deletion crates/ullm/src/bin/ullm.rs → crates/cli/bin/ullm.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::Result;
use clap::Parser;
use ullm::cmd::{App, Command, Config};
use ucli::{App, Command, Config};

#[tokio::main]
async fn main() -> Result<()> {
Expand Down
6 changes: 3 additions & 3 deletions crates/ullm/src/cmd/chat.rs → crates/cli/src/chat.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
//! Chat command

use super::Config;
use crate::DeepSeek;
use anyhow::Result;
use clap::{Args, ValueEnum};
use futures_util::StreamExt;
use std::{
fmt::{Display, Formatter},
io::{BufRead, Write},
};
use ucore::{Chat, Client, LLM, Message};
use ullm::DeepSeek;
use ullm::{Chat, Client, LLM, Message};

/// Chat command arguments
#[derive(Debug, Args)]
Expand Down Expand Up @@ -64,7 +64,7 @@ impl ChatCmd {
Ok(())
}

async fn send(chat: &mut Chat<DeepSeek>, message: Message, stream: bool) -> Result<()> {
async fn send(chat: &mut Chat<DeepSeek, ()>, message: Message, stream: bool) -> Result<()> {
if stream {
let mut response_content = String::new();
{
Expand Down
6 changes: 3 additions & 3 deletions crates/ullm/src/cmd/config.rs → crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ static CONFIG: LazyLock<PathBuf> =
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
/// The configuration for the CLI
config: ucore::Config,
config: ullm::General,

/// The API keys for LLMs
pub key: BTreeMap<String, String>,
Expand All @@ -31,15 +31,15 @@ impl Config {
}

/// Get the core config
pub fn config(&self) -> &ucore::Config {
pub fn config(&self) -> &ullm::General {
&self.config
}
}

impl Default for Config {
fn default() -> Self {
Self {
config: ucore::Config::default(),
config: ullm::General::default(),
key: [("deepseek".to_string(), "YOUR_API_KEY".to_string())]
.into_iter()
.collect::<_>(),
Expand Down
File renamed without changes.
6 changes: 3 additions & 3 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ keywords.workspace = true

[dependencies]
anyhow.workspace = true
async-stream.workspace = true
derive_more.workspace = true
serde.workspace = true
serde_json.workspace = true
futures-core.workspace = true
futures-util.workspace = true
reqwest.workspace = true
schemars.workspace = true

[dev-dependencies]
serde_json.workspace = true
52 changes: 52 additions & 0 deletions crates/core/src/agent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! Turbofish Agent library

use crate::{Message, StreamChunk, Tool, ToolCall, ToolChoice, message::ToolMessage};
use anyhow::Result;

/// A trait for turbofish agents
///
/// TODO: add schemar for request and response
pub trait Agent: Clone {
/// The parsed chunk from [StreamChunk]
type Chunk;

/// The system prompt for the agent
const SYSTEM_PROMPT: &str;

/// The tools for the agent
const TOOLS: Vec<Tool> = Vec::new();

/// Filter the messages to match required tools for the agent
fn filter(&self, _message: &str) -> ToolChoice {
ToolChoice::Auto
}

/// Dispatch tool calls
fn dispatch(&self, tools: &[ToolCall]) -> impl Future<Output = Vec<ToolMessage>> {
async move {
tools
.iter()
.map(|tool| ToolMessage {
tool: tool.id.clone(),
message: Message::tool(format!(
"function {} not available",
tool.function.name
)),
})
.collect()
}
}

/// Parse a chunk from [StreamChunk]
fn chunk(&self, chunk: &StreamChunk) -> impl Future<Output = Result<Self::Chunk>>;
}

impl Agent for () {
type Chunk = StreamChunk;

const SYSTEM_PROMPT: &str = "You are a helpful assistant.";

async fn chunk(&self, chunk: &StreamChunk) -> Result<Self::Chunk> {
Ok(chunk.clone())
}
}
129 changes: 122 additions & 7 deletions crates/core/src/chat.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,145 @@
//! Chat abstractions for the unified LLM Interfaces

use crate::{
LLM, Response, Role, StreamChunk,
Agent, Config, FinishReason, General, LLM, Response, Role,
message::{AssistantMessage, Message, ToolMessage},
};
use anyhow::Result;
use futures_core::Stream;
use futures_util::StreamExt;
use serde::Serialize;

const MAX_TOOL_CALLS: usize = 16;

/// A chat for the LLM
pub struct Chat<P: LLM> {
#[derive(Clone)]
pub struct Chat<P: LLM, A: Agent> {
/// The chat configuration
pub config: P::ChatConfig,

/// Chat history in memory
pub messages: Vec<ChatMessage>,

/// The LLM provider
pub provider: P,
provider: P,

/// The agent
agent: A,

/// Whether to return the usage information in stream mode
usage: bool,
}

impl<P: LLM> Chat<P, ()> {
/// Create a new chat
pub fn new(config: General, provider: P) -> Self {
Self {
messages: vec![],
provider,
usage: config.usage,
agent: (),
config: config.into(),
}
}
}

impl<P: LLM> Chat<P> {
impl<P: LLM, A: Agent> Chat<P, A> {
/// Add the system prompt to the chat
pub fn system<B: Agent>(mut self, agent: B) -> Chat<P, B> {
let mut messages = self.messages;
if messages.is_empty() {
messages.push(Message::system(A::SYSTEM_PROMPT).into());
} else if let Some(ChatMessage::System(_)) = messages.first() {
messages.insert(0, Message::system(A::SYSTEM_PROMPT).into());
} else {
messages = vec![Message::system(A::SYSTEM_PROMPT).into()]
.into_iter()
.chain(messages)
.collect();
}

self.config = self.config.with_tools(A::TOOLS);
Chat {
messages,
provider: self.provider,
usage: self.usage,
agent,
config: self.config,
}
}

/// Send a message to the LLM
pub async fn send(&mut self, message: Message) -> Result<Response> {
let config = self
.config
.with_tool_choice(self.agent.filter(message.content.as_str()));
self.messages.push(message.into());
self.provider.send(&self.config, &self.messages).await

for _ in 0..MAX_TOOL_CALLS {
let response = self.provider.send(&config, &self.messages).await?;
let Some(tool_calls) = response.tool_calls() else {
return Ok(response);
};

let result = self.agent.dispatch(tool_calls).await;
self.messages.extend(result.into_iter().map(Into::into));
}

anyhow::bail!("max tool calls reached");
}

/// Send a message to the LLM with streaming
pub fn stream(&mut self, message: Message) -> impl Stream<Item = Result<StreamChunk>> {
pub fn stream(
&mut self,
message: Message,
) -> impl Stream<Item = Result<A::Chunk>> + use<'_, P, A> {
let config = self
.config
.with_tool_choice(self.agent.filter(message.content.as_str()));
self.messages.push(message.into());
self.provider.stream(&self.config, &self.messages)

async_stream::try_stream! {
for _ in 0..MAX_TOOL_CALLS {
let messages = self.messages.clone();
let inner = self.provider.stream(config.clone(), &messages, self.usage);
futures_util::pin_mut!(inner);

let mut tool_calls = None;
let mut message = String::new();
while let Some(chunk) = inner.next().await {
let chunk = chunk?;
if let Some(calls) = chunk.tool_calls() {
tool_calls = Some(calls.to_vec());
}

if let Some(content) = chunk.content() {
message.push_str(content);
}

yield self.agent.chunk(&chunk).await?;
if let Some(reason) = chunk.reason() {
match reason {
FinishReason::Stop => return,
FinishReason::ToolCalls => break,
reason => Err(anyhow::anyhow!("unexpected finish reason: {reason:?}"))?,
}
}
}

if !message.is_empty() {
self.messages.push(Message::assistant(&message).into());
}

if let Some(calls) = tool_calls {
let result = self.agent.dispatch(&calls).await;
self.messages.extend(result.into_iter().map(Into::into));
} else {
break;
}
}

Err(anyhow::anyhow!("max tool calls reached"))?;
}
}
}

Expand Down Expand Up @@ -68,3 +177,9 @@ impl From<Message> for ChatMessage {
}
}
}

impl From<ToolMessage> for ChatMessage {
fn from(message: ToolMessage) -> Self {
ChatMessage::Tool(message)
}
}
Loading