use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::engine::{estimate_tokens, stable_id, FormalAiEngine, SymbolicAnswer, DEFAULT_MODEL};
use crate::solver::{ConversationTurn, UniversalSolver};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatCompletionRequest {
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub messages: Vec<ChatMessage>,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub stream: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub functions: Vec<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub function_call: Option<Value>,
}
impl ChatCompletionRequest {
#[must_use]
pub fn requests_tool_execution(&self) -> bool {
if self
.tool_choice
.as_ref()
.is_some_and(is_tool_choice_request)
|| self
.function_call
.as_ref()
.is_some_and(is_tool_choice_request)
{
return true;
}
let tool_calls_disabled = self
.tool_choice
.as_ref()
.is_some_and(matches_tool_choice_none);
let function_calls_disabled = self
.function_call
.as_ref()
.is_some_and(matches_tool_choice_none);
(!self.tools.is_empty() && !tool_calls_disabled)
|| (!self.functions.is_empty() && !function_calls_disabled)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: String,
pub content: MessageContent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<MessageContentPart>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageContentPart {
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub text: Option<String>,
}
impl MessageContent {
#[must_use]
pub fn plain_text(&self) -> String {
match self {
Self::Text(text) => text.clone(),
Self::Parts(parts) => parts
.iter()
.filter_map(|part| part.text.as_deref())
.collect::<Vec<_>>()
.join("\n"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatCompletion {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<ChatChoice>,
pub usage: TokenUsage,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatChoice {
pub index: u32,
pub message: ChatMessage,
pub finish_reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResponsesRequest {
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub input: Value,
#[serde(default)]
pub instructions: Option<String>,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub stream: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseObject {
pub id: String,
pub object: String,
pub created_at: u64,
pub status: String,
pub model: String,
pub output: Vec<ResponseOutputMessage>,
pub usage: ResponseUsage,
#[serde(default)]
pub evidence_links: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseOutputMessage {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
pub role: String,
pub content: Vec<ResponseOutputContent>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseOutputContent {
#[serde(rename = "type")]
pub kind: String,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseUsage {
pub input_tokens: u32,
pub output_tokens: u32,
pub total_tokens: u32,
}
#[must_use]
pub fn create_chat_completion(request: &ChatCompletionRequest) -> ChatCompletion {
create_chat_completion_with_solver(request, &UniversalSolver::default())
}
#[must_use]
pub fn create_chat_completion_with_solver(
request: &ChatCompletionRequest,
solver: &UniversalSolver,
) -> ChatCompletion {
let (prompt, history) = chat_prompt_and_history(&request.messages);
let symbolic_answer = if request.requests_tool_execution() && !solver.config.agent_mode {
tool_call_refusal_answer()
} else {
solver.solve_with_history(&prompt, &history)
};
chat_completion_from_symbolic(request, &prompt, symbolic_answer)
}
fn chat_completion_from_symbolic(
request: &ChatCompletionRequest,
prompt: &str,
symbolic_answer: SymbolicAnswer,
) -> ChatCompletion {
let model = request
.model
.clone()
.unwrap_or_else(|| String::from(DEFAULT_MODEL));
let prompt_tokens = estimate_tokens(prompt);
let completion_tokens = estimate_tokens(&symbolic_answer.answer);
ChatCompletion {
id: stable_id("chatcmpl", prompt),
object: String::from("chat.completion"),
created: 0,
model,
choices: vec![ChatChoice {
index: 0,
message: ChatMessage {
role: String::from("assistant"),
content: MessageContent::Text(symbolic_answer.answer),
},
finish_reason: String::from("stop"),
}],
usage: TokenUsage {
prompt_tokens,
completion_tokens,
total_tokens: prompt_tokens.saturating_add(completion_tokens),
},
}
}
#[must_use]
pub fn create_response(request: &ResponsesRequest) -> ResponseObject {
let prompt = response_prompt(request);
let symbolic_answer = FormalAiEngine.answer(&prompt);
response_from_symbolic(request, &prompt, symbolic_answer)
}
#[must_use]
pub fn create_response_with_solver(
request: &ResponsesRequest,
solver: &UniversalSolver,
) -> ResponseObject {
let prompt = response_prompt(request);
let symbolic_answer = solver.solve(&prompt);
response_from_symbolic(request, &prompt, symbolic_answer)
}
fn response_from_symbolic(
request: &ResponsesRequest,
prompt: &str,
symbolic_answer: SymbolicAnswer,
) -> ResponseObject {
let model = request
.model
.clone()
.unwrap_or_else(|| String::from(DEFAULT_MODEL));
let input_tokens = estimate_tokens(prompt);
let output_tokens = estimate_tokens(&symbolic_answer.answer);
ResponseObject {
id: stable_id("resp", prompt),
object: String::from("response"),
created_at: 0,
status: String::from("completed"),
model,
output: vec![ResponseOutputMessage {
id: stable_id("msg", &symbolic_answer.answer),
kind: String::from("message"),
role: String::from("assistant"),
content: vec![ResponseOutputContent {
kind: String::from("output_text"),
text: symbolic_answer.answer,
}],
}],
usage: ResponseUsage {
input_tokens,
output_tokens,
total_tokens: input_tokens.saturating_add(output_tokens),
},
evidence_links: symbolic_answer.evidence_links,
}
}
fn chat_prompt_and_history(messages: &[ChatMessage]) -> (String, Vec<ConversationTurn>) {
let Some(latest_user_index) = messages
.iter()
.rposition(|message| message.role.eq_ignore_ascii_case("user"))
else {
return (String::new(), Vec::new());
};
let prompt = messages[latest_user_index].content.plain_text();
let history = messages[..latest_user_index]
.iter()
.filter_map(chat_message_to_turn)
.collect();
(prompt, history)
}
fn chat_message_to_turn(message: &ChatMessage) -> Option<ConversationTurn> {
let content = message.content.plain_text();
if content.trim().is_empty() {
return None;
}
if message.role.eq_ignore_ascii_case("user") {
return Some(ConversationTurn::user(content));
}
if message.role.eq_ignore_ascii_case("assistant") {
return Some(ConversationTurn::assistant(content));
}
None
}
fn tool_call_refusal_answer() -> SymbolicAnswer {
SymbolicAnswer {
intent: String::from("tool_call_refused"),
answer: String::from(
"Tool calls and function execution are not allowed without explicit agent mode. \
Enable agent mode only for an isolated execution environment.",
),
confidence: 1.0,
evidence_links: vec![String::from("policy:agent_mode_required_for_tools")],
links_notation: String::from(
"tool_call_refusal\n policy \"agent_mode_required_for_tools\"\n",
),
}
}
fn is_tool_choice_request(value: &Value) -> bool {
!matches_tool_choice_none(value)
}
fn matches_tool_choice_none(value: &Value) -> bool {
match value {
Value::Null => true,
Value::String(choice) => choice.eq_ignore_ascii_case("none"),
Value::Object(object) => object
.get("type")
.and_then(Value::as_str)
.is_some_and(|kind| kind.eq_ignore_ascii_case("none")),
_ => false,
}
}
fn response_prompt(request: &ResponsesRequest) -> String {
let input = value_to_prompt_text(&request.input);
match request.instructions.as_deref() {
Some(instructions) if !instructions.trim().is_empty() => {
format!("{}\n{}", instructions.trim(), input.trim())
}
_ => input,
}
}
fn value_to_prompt_text(value: &Value) -> String {
match value {
Value::String(text) => text.clone(),
Value::Array(items) => items
.iter()
.map(value_to_prompt_text)
.filter(|text| !text.trim().is_empty())
.collect::<Vec<_>>()
.join("\n"),
Value::Object(object) => object
.get("content")
.or_else(|| object.get("text"))
.map_or_else(String::new, value_to_prompt_text),
_ => String::new(),
}
}