leptos/projects/openapi-openai-api-swagger-ui/src/open_ai.rs

267 lines
10 KiB
Rust

/*
Follows
https://cookbook.openai.com/examples/function_calling_with_an_openapi_spec
closely
*/
pub static SYSTEM_MESSAGE :&'static str = "
You are a helpful assistant.
Respond to the following prompt by using function_call and then summarize actions.
Ask for clarification if a user request is ambiguous.
";
use serde_json::Map;
use openai_dive::v1::api::Client;
use openai_dive::v1::models::Gpt4Engine;
use std::env;
use openai_dive::v1::resources::chat::{
ChatCompletionFunction, ChatCompletionParameters, ChatCompletionTool, ChatCompletionToolType, ChatMessage,
ChatMessageContent,Role,
};
use utoipa::openapi::schema::Array;
use serde_json::Value;
use utoipa::openapi::schema::SchemaType;
use utoipa::openapi::schema::Schema;
use utoipa::OpenApi;
use serde_json::json;
use utoipa::openapi::path::{PathItemType,Parameter};
use utoipa::openapi::Required;
use utoipa::openapi::schema::Object;
use utoipa::openapi::RefOr;
pub fn make_openapi_call_via_gpt(message:String) -> ChatCompletionParameters {
let docs = super::api_doc::ApiDoc::openapi();
let mut functions = vec![];
// get each path and it's path item object
for (path,path_item) in docs.paths.paths.iter(){
// all our server functions are post.
let operation = path_item.operations.get(&PathItemType::Post).expect("Expect POST op");
// This name will be given to the OpenAI API as part of our functions
let name = operation.operation_id.clone().expect("Each operation to have an operation id");
// we'll use the description
let desc = operation.description.clone().expect("Each operation to have a description, this is how GPT knows what the functiond does and it is helpful for calling it.");
let mut required_list = vec![];
let mut properties = serde_json::Map::new();
if let Some(params) = operation.parameters.clone() {
leptos::logging::log!("{params:#?}");
for Parameter{name,description,required,schema,..} in params.into_iter() {
if required == Required::True {
required_list.push(name.clone());
}
let description = description.unwrap_or_default();
if let Some(RefOr::Ref(utoipa::openapi::schema::Ref{ref_location,..})) = schema {
let schema_name = ref_location.split('/').last().expect("Expecting last after split");
let RefOr::T(schema) = docs.components
.as_ref()
.expect("components")
.schemas
.get(schema_name)
.cloned()
.expect("{schema_name} to be in components as a schema") else {panic!("expecting T")};
let mut output = Map::new();
parse_schema_into_openapi_property(name.clone(),schema,&mut output);
properties.insert(name,serde_json::Value::Object(output));
} else if let Some(RefOr::T(schema)) = schema {
let mut output = Map::new();
parse_schema_into_openapi_property(name.clone(),schema,&mut output);
properties.insert(name.clone(),serde_json::Value::Object(output));
}
}
}
let parameters = json!({
"type": "object",
"properties": properties,
"required": required_list,
});
leptos::logging::log!("{parameters}");
functions.push(
ChatCompletionFunction {
name,
description: Some(desc),
parameters,
}
)
}
ChatCompletionParameters {
model: Gpt4Engine::Gpt41106Preview.to_string(),
messages: vec![
ChatMessage {
role:Role::System,
content: ChatMessageContent::Text(SYSTEM_MESSAGE.to_string()),
..Default::default()
},
ChatMessage {
role:Role::User,
content: ChatMessageContent::Text(message),
..Default::default()
}],
tools: Some(functions.into_iter().map(|function|{
ChatCompletionTool {
r#type: ChatCompletionToolType::Function,
function,
}
}).collect::<Vec<ChatCompletionTool>>()),
..Default::default()
}
}
pub fn parse_schema_into_openapi_property(
name:String,
schema:Schema,
output: &mut serde_json::Map::<String,serde_json::Value>) {
let docs = super::api_doc::ApiDoc::openapi();
match schema {
Schema::Object(Object{
schema_type,
required,
properties,
..
}) => match schema_type{
SchemaType::Object => {
output.insert("type".to_string(),Value::String("object".to_string()));
output.insert("required".to_string(),Value::Array(required.into_iter()
.map(|s|Value::String(s))
.collect::<Vec<Value>>()));
output.insert("properties".to_string(),{
let mut map = Map::new();
for (key,val) in properties
.into_iter()
.map(|(key,val)|{
let RefOr::T(schema) = val else {panic!("expecting t")};
let mut output = Map::new();
parse_schema_into_openapi_property(name.clone(),schema,&mut output);
(key,output)
}) {
map.insert(key,Value::Object(val));
}
Value::Object(map)
});
},
SchemaType::Value => {
panic!("not expecting Value here.");
},
SchemaType::String => {
output.insert("type".to_string(),serde_json::Value::String("string".to_string()));
},
SchemaType::Integer => {
output.insert("type".to_string(),serde_json::Value::String("integer".to_string()));
},
SchemaType::Number => {
output.insert("type".to_string(),serde_json::Value::String("number".to_string()));
},
SchemaType::Boolean => {
output.insert("type".to_string(),serde_json::Value::String("boolean".to_string()));
},
SchemaType::Array => {
output.insert("type".to_string(),serde_json::Value::String("array".to_string()));
},
},
Schema::Array(Array{schema_type,items,..}) => {
match schema_type {
SchemaType::Array => {
let mut map = Map::new();
if let RefOr::Ref(utoipa::openapi::schema::Ref{ref_location,..}) = *items {
let schema_name = ref_location.split('/').last().expect("Expecting last after split");
let RefOr::T(schema) = docs.components
.as_ref()
.expect("components")
.schemas
.get(schema_name)
.cloned()
.expect("{schema_name} to be in components as a schema") else {panic!("expecting T")};
let mut map = Map::new();
parse_schema_into_openapi_property(name.clone(),schema,&mut map);
output.insert(name.clone(),serde_json::Value::Object(map));
} else if let RefOr::T(schema) = *items {
let mut map = Map::new();
parse_schema_into_openapi_property(name.clone(),schema,&mut map);
output.insert(name,serde_json::Value::Object(map));
}
},
_ => panic!("if schema is an array, then I'm expecting schema type to be an array ")
}
}
_ => panic!("I don't know how to handle this yet.")
}
}
// let docs = super::api_doc::ApiDoc::openapi();
use crate::app::AiServerCall;
pub async fn call_gpt_with_api(message:String) -> Vec<AiServerCall> {
let api_key = std::env::var("OPENAI_API_KEY").expect("$OPENAI_API_KEY is not set");
let client = Client::new(api_key);
let completion_parameters = make_openapi_call_via_gpt(message);
let result = client.chat().create(completion_parameters).await.unwrap();
let message = result.choices[0].message.clone();
let mut res = vec![];
if let Some(tool_calls) = message.clone().tool_calls {
for tool_call in tool_calls {
let name = tool_call.function.name;
let arguments = tool_call.function.arguments;
res.push(AiServerCall{
path:name,
args:arguments,
});
}
}
res
}
/*
def openapi_to_functions(openapi_spec):
functions = []
for path, methods in openapi_spec["paths"].items():
for method, spec_with_ref in methods.items():
# 1. Resolve JSON references.
spec = jsonref.replace_refs(spec_with_ref)
# 2. Extract a name for the functions.
function_name = spec.get("operationId")
# 3. Extract a description and parameters.
desc = spec.get("description") or spec.get("summary", "")
schema = {"type": "object", "properties": {}}
req_body = (
spec.get("requestBody", {})
.get("content", {})
.get("application/json", {})
.get("schema")
)
if req_body:
schema["properties"]["requestBody"] = req_body
params = spec.get("parameters", [])
if params:
param_properties = {
param["name"]: param["schema"]
for param in params
if "schema" in param
}
schema["properties"]["parameters"] = {
"type": "object",
"properties": param_properties,
}
functions.append(
{"type": "function", "function": {"name": function_name, "description": desc, "parameters": schema}}
)
return functions */