From 08b0748c158b3bebce6da0860454240aa19034db Mon Sep 17 00:00:00 2001 From: Ali Naqvi Date: Mon, 16 Oct 2023 14:04:07 +0800 Subject: [PATCH 1/6] feat: PPT-568 Added Controller for ChatBot --- OPENAPI_DOC.yml | 343 ++++++++++++++++++ shard.lock | 10 +- shard.yml | 4 + spec/openai/chatgpt_spec.cr | 83 +++++ src/constants.cr | 6 + src/placeos-rest-api/controllers/openai.cr | 66 ++++ .../controllers/openai/chat_manager.cr | 237 ++++++++++++ 7 files changed, 746 insertions(+), 3 deletions(-) create mode 100644 spec/openai/chatgpt_spec.cr create mode 100644 src/placeos-rest-api/controllers/openai.cr create mode 100644 src/placeos-rest-api/controllers/openai/chat_manager.cr diff --git a/OPENAPI_DOC.yml b/OPENAPI_DOC.yml index 8f744b90..488ec032 100644 --- a/OPENAPI_DOC.yml +++ b/OPENAPI_DOC.yml @@ -10776,6 +10776,312 @@ paths: application/json: schema: $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/chatgpt: + get: + summary: list user chats + tags: + - ChatGPT + operationId: PlaceOS::Api::ChatGPT_index + parameters: + - name: q + in: query + description: returns results based on a [simple query string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html) + required: false + schema: + type: string + - name: limit + in: query + description: the maximum number of results to return + example: "10000" + required: false + schema: + type: integer + format: UInt32 + - name: offset + in: query + description: deprecated, the starting offset of the result set. Used to implement + pagination + required: false + schema: + type: integer + format: UInt32 + - name: ref + in: query + description: a token for accessing the next page of results, provided in the + `Link` header + required: false + schema: + type: string + nullable: true + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PlaceOS__Model__Chat' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/chatgpt/{id}: + get: + summary: show user chat history + tags: + - ChatGPT + operationId: PlaceOS::Api::ChatGPT_show + parameters: + - name: id + in: path + description: return the chat messages associated with this chat id + example: chats-xxxx + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/NamedTuple_role__String__content__String___Nil__timestamp__Time_' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + delete: + summary: remove chat and associated history + tags: + - ChatGPT + operationId: PlaceOS::Api::ChatGPT_destroy + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 202: + description: Accepted + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + /api/engine/v2/chatgpt/chat/{system_id}: + get: + summary: the websocket endpoint for ChatGPT chatbot + tags: + - ChatGPT + operationId: PlaceOS::Api::ChatGPT_chat + parameters: + - name: system_id + in: path + required: true + schema: + type: string + - name: resume + in: query + description: To resume previous chat session. Provide session chat id + example: chats-xxxx + required: false + schema: + type: string + nullable: true + responses: + 101: + description: Switching Protocols + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 408: + description: Request Timeout + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ParameterError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOS__Api__Application__ContentError' /api/engine/v2/repositories: get: summary: lists the repositories added to the system @@ -21111,6 +21417,43 @@ components: type: integer format: Int64 nullable: true + PlaceOS__Model__Chat: + type: object + properties: + created_at: + type: integer + format: Int64 + nullable: true + updated_at: + type: integer + format: Int64 + nullable: true + summary: + type: string + nullable: true + user_id: + type: string + nullable: true + system_id: + type: string + nullable: true + id: + type: string + nullable: true + NamedTuple_role__String__content__String___Nil__timestamp__Time_: + type: object + properties: + role: + type: string + content: + type: string + nullable: true + timestamp: + type: string + format: date-time + required: + - role + - timestamp PlaceOS__Model__Repository: type: object properties: diff --git a/shard.lock b/shard.lock index 07ac66d4..a8b11f5d 100644 --- a/shard.lock +++ b/shard.lock @@ -189,6 +189,10 @@ shards: git: https://github.com/elbywan/open_api.cr.git version: 1.3.0 + openai: + git: https://github.com/spider-gazelle/crystal-openai.git + version: 0.9.0+git.commit.e6bfaba7758f992d7cb81cad0109180d5be2d958 + openapi-generator: git: https://github.com/place-labs/openapi-generator.git version: 2.1.0+git.commit.a65ffc2f7dcc6a393e7d1f9229650b520d9525be @@ -251,7 +255,7 @@ shards: placeos-driver: git: https://github.com/placeos/driver.git - version: 6.9.4 + version: 6.9.9 placeos-frontend-loader: git: https://github.com/placeos/frontend-loader.git @@ -263,7 +267,7 @@ shards: placeos-models: git: https://github.com/placeos/models.git - version: 9.24.1 + version: 9.25.0 placeos-resource: git: https://github.com/place-labs/resource.git @@ -311,7 +315,7 @@ shards: search-ingest: git: https://github.com/placeos/search-ingest.git - version: 2.10.3+git.commit.218c93b0077be2c3bfbe272f5ee11d6fb6b7fef1 + version: 2.10.4+git.commit.ad86e414f43fa5debb28192b0a01c7d3f655ce28 secrets-env: # Overridden git: https://github.com/spider-gazelle/secrets-env.git diff --git a/shard.yml b/shard.yml index 27b7cc71..e68770b9 100644 --- a/shard.yml +++ b/shard.yml @@ -98,6 +98,10 @@ dependencies: upload-signer: github: spider-gazelle/upload-signer + # Crystal OpenAI + openai: + github: spider-gazelle/crystal-openai + development_dependencies: # Linter ameba: diff --git a/spec/openai/chatgpt_spec.cr b/spec/openai/chatgpt_spec.cr new file mode 100644 index 00000000..a0b753bb --- /dev/null +++ b/spec/openai/chatgpt_spec.cr @@ -0,0 +1,83 @@ +require "../helper" + +module PlaceOS::Api + describe ChatGPT, focus: true do + ::Spec.before_each do + Model::ChatMessage.clear + Model::Chat.clear + end + + Spec.test_404(ChatGPT.base_route, model_name: Model::Chat.table_name, headers: Spec::Authentication.headers) + + it "GET should return an empty list of chats for users without any chat history" do + resp = client.get("#{ChatGPT.base_route}", + headers: Spec::Authentication.headers) + + resp.status_code.should eq(200) + + JSON.parse(resp.body).size.should eq(0) + end + + it "GET should return list of chats" do + user = Spec::Authentication.user + PlaceOS::Model::Generator.chat(user: user).save! + + resp = client.get("#{ChatGPT.base_route}", + headers: Spec::Authentication.headers) + + resp.status_code.should eq(200) + + JSON.parse(resp.body).size.should eq(1) + end + + it "GET should return list of chat history" do + user = Spec::Authentication.user + chat = PlaceOS::Model::Generator.chat(user: user).save! + PlaceOS::Model::Generator.chat_message(chat: chat).save! + + resp = client.get("#{ChatGPT.base_route}#{chat.id}", + headers: Spec::Authentication.headers) + + resp.status_code.should eq(200) + hist = JSON.parse(resp.body).as_a + hist.size.should eq(1) + hist.first.as_h["role"].should eq("user") + end + + it "GET should return list of chat history and filter out system messages" do + user = Spec::Authentication.user + chat = PlaceOS::Model::Generator.chat(user: user).save! + PlaceOS::Model::Generator.chat_message(chat: chat).save! + + PlaceOS::Model::Generator.chat_message(chat: chat, role: PlaceOS::Model::ChatMessage::Role::Assistant).save! + PlaceOS::Model::Generator.chat_message(chat: chat, role: PlaceOS::Model::ChatMessage::Role::System).save! + PlaceOS::Model::Generator.chat_message(chat: chat, role: PlaceOS::Model::ChatMessage::Role::Function).save! + + resp = client.get("#{ChatGPT.base_route}#{chat.id}", + headers: Spec::Authentication.headers) + + resp.status_code.should eq(200) + hist = JSON.parse(resp.body).as_a + hist.size.should eq(2) + hist.first.as_h["role"].should eq("user") + hist.last.as_h["role"].should eq("assistant") + end + + it "deleting chat should delete all associated history" do + chat = PlaceOS::Model::Generator.chat.save! + PlaceOS::Model::Generator.chat_message(chat: chat).save! + + PlaceOS::Model::Generator.chat_message(chat: chat, role: PlaceOS::Model::ChatMessage::Role::Assistant).save! + PlaceOS::Model::Generator.chat_message(chat: chat, role: PlaceOS::Model::ChatMessage::Role::System).save! + + PlaceOS::Model::ChatMessage.where(chat_id: chat.id).count.should eq(3) + + resp = client.delete("#{ChatGPT.base_route}#{chat.id}", + headers: Spec::Authentication.headers) + + resp.status_code.should eq(202) + + PlaceOS::Model::ChatMessage.where(chat_id: chat.id).count.should eq(0) + end + end +end diff --git a/src/constants.cr b/src/constants.cr index 42e5fac7..7bdb26ea 100644 --- a/src/constants.cr +++ b/src/constants.cr @@ -30,6 +30,12 @@ module PlaceOS::Api PROD = ENV["SG_ENV"]?.try(&.downcase) == "production" + # Open AI + OPENAI_API_KEY = ENV["OPENAI_API_KEY"]? + OPENAI_API_KEY_PATH = ENV["OPENAI_API_KEY_PATH"]? + ORGANIZATION = ENV["OPENAI_ORGANIZATION"]? || "" + OPENAI_API_BASE = ENV["OPENAI_API_BASE"]? # Set this to Azure URL only if Azure OpenAI is used + # CHANGELOG ################################################################################################# diff --git a/src/placeos-rest-api/controllers/openai.cr b/src/placeos-rest-api/controllers/openai.cr new file mode 100644 index 00000000..bcb8819b --- /dev/null +++ b/src/placeos-rest-api/controllers/openai.cr @@ -0,0 +1,66 @@ +require "./application" + +module PlaceOS::Api + class ChatGPT < Application + include Utils::CoreHelper + alias RemoteDriver = ::PlaceOS::Driver::Proxy::RemoteDriver + + base "/api/engine/v2/chatgpt/" + + before_action :can_read, only: [:index, :show] + before_action :can_write, only: [:chat, :delete] + + getter chat_manager : ChatGPT::ChatManager { ChatGPT::ChatManager.new(self) } + + # list user chats + @[AC::Route::GET("/")] + def index : Array(Model::Chat) + Model::Chat.where(user_id: current_user.id.not_nil!).all.to_a + end + + # show user chat history + @[AC::Route::GET("/:id")] + def show( + @[AC::Param::Info(name: "id", description: "return the chat messages associated with this chat id", example: "chats-xxxx")] + id : String + ) : Array(NamedTuple(role: String, content: String?, timestamp: Time)) + unless chat = Model::Chat.find?(id) + Log.warn { {message: "Invalid chat id. Unable to find matching chat history", id: id, user: current_user.id} } + raise Error::NotFound.new("Invalid chat id: #{id}") + end + + chat.messages.to_a.select!(&.role.in?([Model::ChatMessage::Role::User, Model::ChatMessage::Role::Assistant])) + .map { |c| {role: c.role.to_s, content: c.content, timestamp: c.created_at} } + end + + # the websocket endpoint for ChatGPT chatbot + @[AC::Route::WebSocket("/chat/:system_id")] + def chat(socket, system_id : String, + @[AC::Param::Info(name: "resume", description: "To resume previous chat session. Provide session chat id", example: "chats-xxxx")] + resume : String? = nil) : Nil + chat = (resume && PlaceOS::Model::Chat.find!(resume.not_nil!)) || begin + PlaceOS::Model::Chat.create!(user_id: current_user.id.as(String), system_id: system_id, summary: "") + end + + begin + chat_manager.start_chat(socket, chat, !!resume) + rescue e : RemoteDriver::Error + handle_execute_error(e) + rescue e + render_error(HTTP::Status::INTERNAL_SERVER_ERROR, e.message, backtrace: e.backtrace) + end + end + + # remove chat and associated history + @[AC::Route::DELETE("/:id", status_code: HTTP::Status::ACCEPTED)] + def destroy(id : String) : Nil + unless chat = Model::Chat.find?(id) + Log.warn { {message: "Invalid chat id. Unable to find matching chat record", id: id, user: current_user.id} } + raise Error::NotFound.new("Invalid chat id: #{id}") + end + chat.destroy + end + end +end + +require "./openai/chat_manager" diff --git a/src/placeos-rest-api/controllers/openai/chat_manager.cr b/src/placeos-rest-api/controllers/openai/chat_manager.cr new file mode 100644 index 00000000..f097eafd --- /dev/null +++ b/src/placeos-rest-api/controllers/openai/chat_manager.cr @@ -0,0 +1,237 @@ +require "tasker" +require "mutex" +require "openai" + +module PlaceOS::Api + class ChatGPT::ChatManager + Log = ::Log.for(self) + alias RemoteDriver = ::PlaceOS::Driver::Proxy::RemoteDriver + + private getter chat_sockets = {} of String => {HTTP::WebSocket, OpenAI::Client, OpenAI::ChatCompletionRequest, OpenAI::FunctionExecutor} + private getter ping_tasks : Hash(String, Tasker::Repeat(Nil)) = {} of String => Tasker::Repeat(Nil) + + private getter ws_lock = Mutex.new(protection: :reentrant) + private getter session_ch = Channel(Nil).new + private getter app : ChatGPT + + LLM_DRIVER = "LLM" + LLM_DRIVER_CHAT = "new_chat" + + def initialize(@app) + end + + def start_chat(ws : HTTP::WebSocket, chat : PlaceOS::Model::Chat, resume : Bool = false) + chat_id = chat.id.as(String) + update_summary = !resume + chat_prompt = + if resume + Log.debug { {chat_id: chat_id, message: "resuming chat session"} } + nil + else + Log.debug { {chat_id: chat_id, message: "starting new chat session"} } + driver_prompt(chat) + end + + ws_lock.synchronize do + if existing_socket = chat_sockets[chat_id]? + existing_socket[0].close rescue nil + end + + client, executor, chat_completion = setup(chat, chat_prompt) + chat_sockets[chat_id] = {ws, client, chat_completion, executor} + + ping_tasks[chat_id] = Tasker.every(10.seconds) do + ws.ping rescue nil + nil + end + + ws.on_message do |message| + if (update_summary) + PlaceOS::Model::Chat.update(chat_id, {summary: message}) + update_summary = false + end + resp = openai_interaction(client, chat_completion, executor, message, chat_id) + ws.send(resp.to_json) + end + + ws.on_close do + if task = ping_tasks.delete(chat_id) + task.cancel + end + chat_sockets.delete(chat_id) + end + end + end + + private def setup(chat, chat_prompt) + client = build_client + executor = build_executor(chat) + chat_completion = build_completion(build_prompt(chat, chat_prompt), executor.functions) + + {client, executor, chat_completion} + end + + private def build_client + if azure = PlaceOS::Api::OPENAI_API_BASE + OpenAI::Client.azure(api_key: nil, api_endpoint: azure) + else + OpenAI::Client.new + end + end + + private def build_completion(messages, functions) + OpenAI::ChatCompletionRequest.new( + model: OpenAI::GPT3Dot5Turbo, # gpt-3.5-turbo + messages: messages, + functions: functions, + function_call: "auto" + ) + end + + private def openai_interaction(client, request, executor, message, chat_id) : NamedTuple(chat_id: String, message: String?) + request.messages << OpenAI::ChatMessage.new(role: :user, content: message) + save_history(chat_id, :user, message) + loop do + resp = client.chat_completion(request) + msg = resp.choices.first.message + request.messages << msg + save_history(chat_id, msg) + + if func_call = msg.function_call + func_res = executor.execute(func_call) + request.messages << func_res + save_history(chat_id, msg) + next + end + break {chat_id: chat_id, message: msg.content} + end + end + + private def save_history(chat_id : String, role : PlaceOS::Model::ChatMessage::Role, message : String, func_name : String? = nil, func_args : JSON::Any? = nil) : Nil + PlaceOS::Model::ChatMessage.create!(role: role, chat_id: chat_id, content: message, function_name: func_name, function_args: func_args) + end + + private def save_history(chat_id : String, msg : OpenAI::ChatMessage) + save_history(chat_id, PlaceOS::Model::ChatMessage::Role.parse(msg.role.to_s), msg.content || "", msg.name, msg.function_call.try &.arguments) + end + + private def build_prompt(chat : PlaceOS::Model::Chat, chat_prompt : ChatPrompt?) + messages = [] of OpenAI::ChatMessage + + if prompt = chat_prompt + messages << OpenAI::ChatMessage.new(role: :assistant, content: prompt.payload.prompt) + messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have the following capabilities: #{prompt.payload.capabilities.to_json}") + messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have access to the following API: #{function_schemas(chat, prompt.payload.capabilities).to_json}") + messages << OpenAI::ChatMessage.new(role: :assistant, content: "If you were asked to perform any function of given capabilities, perform the action and reply with a confirmation telling what you have done.") + + messages.each { |m| save_history(chat.id.as(String), m) } + else + chat.messages.each do |hist| + func_call = nil + if hist.role.to_s == "function" + if name = hist.function_name + args = hist.function_args || JSON::Any.new(nil) + func_call = OpenAI::ChatFunctionCall.new(name, args) + end + end + messages << OpenAI::ChatMessage.new(role: OpenAI::ChatMessageRole.parse(hist.role.to_s), content: hist.content, + name: hist.function_name, + function_call: func_call + ) + end + end + + messages + end + + private def driver_prompt(chat : PlaceOS::Model::Chat) : ChatPrompt? + resp, code = exec_driver_func(chat, LLM_DRIVER, LLM_DRIVER_CHAT, nil) + if code > 200 && code < 299 + ChatPrompt.new(message: "", payload: Payload.from_json(resp)) + end + end + + private def build_executor(chat) + executor = OpenAI::FunctionExecutor.new + executor.add( + name: "call_driver_func", + description: "Executes functionality offered by driver", + clz: DriverExecutor) do |call| + request = call.as(DriverExecutor) + reply = "No response received" + begin + resp, code = exec_driver_func(chat, request.id, request.driver_func, request.args) + reply = resp if 200 <= code <= 299 + rescue ex + Log.error(exception: ex) { {id: request.id, function: request.driver_func, args: request.args.to_s} } + reply = "Encountered error: #{ex.message}" + end + DriverResponse.new(reply).as(JSON::Serializable) + end + executor + end + + private def function_schemas(chat, capabilities) + schemas = Array(NamedTuple(function: String, description: String, parameters: Hash(String, JSON::Any))).new + capabilities.each do |capability| + resp, code = exec_driver_func(chat, capability.id, "function_schemas", nil) + if code > 200 && code < 299 + schemas += JSON.parse(resp).as_a + end + end + schemas + end + + private def exec_driver_func(chat, module_name, method, args) + remote_driver = RemoteDriver.new( + sys_id: chat.system_id, + module_name: module_name, + index: 1, + discovery: app.class.core_discovery, + user_id: chat.user_id, + ) { |module_id| + Model::Module.find!(module_id).edge_id.as(String) + } + + remote_driver.exec( + security: app.driver_clearance(app.user_token), + function: method, + args: args + ) + end + + private struct DriverExecutor + extend OpenAI::FuncMarker + include JSON::Serializable + + @[JSON::Field(description: "The ID of the driver which provides the functionality")] + getter id : String + + @[JSON::Field(description: "The name of the driver function which will be invoked to perform action. Value placeholders must be replaced with actual values")] + getter driver_func : String + + @[JSON::Field(description: "A string representation of the JSON that should be sent as the arguments to driver function")] + getter args : JSON::Any? + end + + private record DriverResponse, body : String do + include JSON::Serializable + end + + record ChatPrompt, message : String, payload : Payload do + include JSON::Serializable + end + + struct Payload + include JSON::Serializable + + getter prompt : String + getter capabilities : Array(Capabilities) + getter system_id : String + + record Capabilities, id : String, capability : String do + include JSON::Serializable + end + end + end +end From 473fb3fc53659817dba4d9afdcf06df1fea33905 Mon Sep 17 00:00:00 2001 From: Ali Naqvi Date: Tue, 17 Oct 2023 13:24:39 +0800 Subject: [PATCH 2/6] refactoring --- spec/openai/chatgpt_spec.cr | 2 +- src/constants.cr | 6 +- .../controllers/{openai.cr => chat_gpt.cr} | 37 +++++--- .../controllers/openai/chat_manager.cr | 95 ++++++++++--------- 4 files changed, 77 insertions(+), 63 deletions(-) rename src/placeos-rest-api/controllers/{openai.cr => chat_gpt.cr} (65%) diff --git a/spec/openai/chatgpt_spec.cr b/spec/openai/chatgpt_spec.cr index a0b753bb..566ffc85 100644 --- a/spec/openai/chatgpt_spec.cr +++ b/spec/openai/chatgpt_spec.cr @@ -1,7 +1,7 @@ require "../helper" module PlaceOS::Api - describe ChatGPT, focus: true do + describe ChatGPT do ::Spec.before_each do Model::ChatMessage.clear Model::Chat.clear diff --git a/src/constants.cr b/src/constants.cr index 7bdb26ea..a5bf01b0 100644 --- a/src/constants.cr +++ b/src/constants.cr @@ -31,10 +31,8 @@ module PlaceOS::Api PROD = ENV["SG_ENV"]?.try(&.downcase) == "production" # Open AI - OPENAI_API_KEY = ENV["OPENAI_API_KEY"]? - OPENAI_API_KEY_PATH = ENV["OPENAI_API_KEY_PATH"]? - ORGANIZATION = ENV["OPENAI_ORGANIZATION"]? || "" - OPENAI_API_BASE = ENV["OPENAI_API_BASE"]? # Set this to Azure URL only if Azure OpenAI is used + OPENAI_API_KEY = ENV["OPENAI_API_KEY"]? + OPENAI_API_BASE = ENV["OPENAI_API_BASE"]? # Set this to Azure URL only if Azure OpenAI is used # CHANGELOG ################################################################################################# diff --git a/src/placeos-rest-api/controllers/openai.cr b/src/placeos-rest-api/controllers/chat_gpt.cr similarity index 65% rename from src/placeos-rest-api/controllers/openai.cr rename to src/placeos-rest-api/controllers/chat_gpt.cr index bcb8819b..f10371c1 100644 --- a/src/placeos-rest-api/controllers/openai.cr +++ b/src/placeos-rest-api/controllers/chat_gpt.cr @@ -10,7 +10,16 @@ module PlaceOS::Api before_action :can_read, only: [:index, :show] before_action :can_write, only: [:chat, :delete] + @[AC::Route::Filter(:before_action)] + def check_authority + unless @authority = current_authority + Log.warn { {message: "authority not found", action: "authorize!", host: request.hostname} } + raise Error::Unauthorized.new "authority not found" + end + end + getter chat_manager : ChatGPT::ChatManager { ChatGPT::ChatManager.new(self) } + getter! authority : Model::Authority? # list user chats @[AC::Route::GET("/")] @@ -38,17 +47,11 @@ module PlaceOS::Api def chat(socket, system_id : String, @[AC::Param::Info(name: "resume", description: "To resume previous chat session. Provide session chat id", example: "chats-xxxx")] resume : String? = nil) : Nil - chat = (resume && PlaceOS::Model::Chat.find!(resume.not_nil!)) || begin - PlaceOS::Model::Chat.create!(user_id: current_user.id.as(String), system_id: system_id, summary: "") - end - - begin - chat_manager.start_chat(socket, chat, !!resume) - rescue e : RemoteDriver::Error - handle_execute_error(e) - rescue e - render_error(HTTP::Status::INTERNAL_SERVER_ERROR, e.message, backtrace: e.backtrace) - end + chat_manager.start_session(socket, (resume && PlaceOS::Model::Chat.find!(resume.not_nil!)) || nil, system_id) + rescue e : RemoteDriver::Error + handle_execute_error(e) + rescue e + render_error(HTTP::Status::INTERNAL_SERVER_ERROR, e.message, backtrace: e.backtrace) end # remove chat and associated history @@ -60,6 +63,18 @@ module PlaceOS::Api end chat.destroy end + + record Config, api_key : String, api_base : String? + + protected def config + if internals = authority.internals["openai"] + key = internals["api_key"]?.try &.as_s || Api::OPENAI_API_KEY || raise Error::NotFound.new("missing openai api_key configuration") + Config.new(key, internals["api_base"]?.try &.as_s || Api::OPENAI_API_BASE) + else + key = Api::OPENAI_API_KEY || raise Error::NotFound.new("missing openai api_key configuration") + Config.new(key, Api::OPENAI_API_BASE) + end + end end end diff --git a/src/placeos-rest-api/controllers/openai/chat_manager.cr b/src/placeos-rest-api/controllers/openai/chat_manager.cr index f097eafd..b93ac90a 100644 --- a/src/placeos-rest-api/controllers/openai/chat_manager.cr +++ b/src/placeos-rest-api/controllers/openai/chat_manager.cr @@ -7,11 +7,10 @@ module PlaceOS::Api Log = ::Log.for(self) alias RemoteDriver = ::PlaceOS::Driver::Proxy::RemoteDriver - private getter chat_sockets = {} of String => {HTTP::WebSocket, OpenAI::Client, OpenAI::ChatCompletionRequest, OpenAI::FunctionExecutor} - private getter ping_tasks : Hash(String, Tasker::Repeat(Nil)) = {} of String => Tasker::Repeat(Nil) + private getter ws_sockets = {} of UInt64 => {HTTP::WebSocket, String, OpenAI::Client, OpenAI::ChatCompletionRequest, OpenAI::FunctionExecutor} + private getter ws_ping_tasks : Hash(UInt64, Tasker::Repeat(Nil)) = {} of UInt64 => Tasker::Repeat(Nil) private getter ws_lock = Mutex.new(protection: :reentrant) - private getter session_ch = Channel(Nil).new private getter app : ChatGPT LLM_DRIVER = "LLM" @@ -20,63 +19,69 @@ module PlaceOS::Api def initialize(@app) end - def start_chat(ws : HTTP::WebSocket, chat : PlaceOS::Model::Chat, resume : Bool = false) - chat_id = chat.id.as(String) - update_summary = !resume - chat_prompt = - if resume - Log.debug { {chat_id: chat_id, message: "resuming chat session"} } - nil - else - Log.debug { {chat_id: chat_id, message: "starting new chat session"} } - driver_prompt(chat) - end - + def start_session(ws : HTTP::WebSocket, existing_chat : PlaceOS::Model::Chat?, system_id : String) ws_lock.synchronize do - if existing_socket = chat_sockets[chat_id]? + ws_id = ws.object_id + if existing_socket = ws_sockets[ws_id]? existing_socket[0].close rescue nil end - client, executor, chat_completion = setup(chat, chat_prompt) - chat_sockets[chat_id] = {ws, client, chat_completion, executor} + if chat = existing_chat + Log.debug { {chat_id: chat.id, message: "resuming chat session"} } + client, executor, chat_completion = setup(chat, nil) + ws_sockets[ws_id] = {ws, chat.id.as(String), client, chat_completion, executor} + else + Log.debug { {message: "starting new chat session"} } + end - ping_tasks[chat_id] = Tasker.every(10.seconds) do + ws_ping_tasks[ws_id] = Tasker.every(10.seconds) do ws.ping rescue nil nil end - ws.on_message do |message| - if (update_summary) - PlaceOS::Model::Chat.update(chat_id, {summary: message}) - update_summary = false - end - resp = openai_interaction(client, chat_completion, executor, message, chat_id) - ws.send(resp.to_json) - end + ws.on_message { |message| manage_chat(ws, message, system_id) } ws.on_close do - if task = ping_tasks.delete(chat_id) + if task = ws_ping_tasks.delete(ws_id) task.cancel end - chat_sockets.delete(chat_id) + ws_sockets.delete(ws_id) end end end - private def setup(chat, chat_prompt) + private def manage_chat(ws : HTTP::WebSocket, message : String, system_id : String) + ws_lock.synchronize do + ws_id = ws.object_id + _, chat_id, client, completion_req, executor = ws_sockets[ws_id]? || begin + chat = PlaceOS::Model::Chat.create!(user_id: app.current_user.id.as(String), system_id: system_id, summary: message) + id = chat.id.as(String) + c, e, req = setup(chat, driver_prompt(chat)) + ws_sockets[ws_id] = {ws, id, c, req, e} + {ws, id, c, req, e} + end + resp = openai_interaction(client, completion_req, executor, message, chat_id) + ws.send(resp.to_json) + end + end + + private def setup(chat, chat_payload) client = build_client executor = build_executor(chat) - chat_completion = build_completion(build_prompt(chat, chat_prompt), executor.functions) + chat_completion = build_completion(build_prompt(chat, chat_payload), executor.functions) {client, executor, chat_completion} end private def build_client - if azure = PlaceOS::Api::OPENAI_API_BASE - OpenAI::Client.azure(api_key: nil, api_endpoint: azure) - else - OpenAI::Client.new - end + app_config = app.config + config = if base = app_config.api_base + OpenAI::Client::Config.azure(api_key: app_config.api_key, api_base: base) + else + OpenAI::Client::Config.default(api_key: app_config.api_key) + end + + OpenAI::Client.new(config) end private def build_completion(messages, functions) @@ -115,13 +120,13 @@ module PlaceOS::Api save_history(chat_id, PlaceOS::Model::ChatMessage::Role.parse(msg.role.to_s), msg.content || "", msg.name, msg.function_call.try &.arguments) end - private def build_prompt(chat : PlaceOS::Model::Chat, chat_prompt : ChatPrompt?) + private def build_prompt(chat : PlaceOS::Model::Chat, chat_payload : Payload?) messages = [] of OpenAI::ChatMessage - if prompt = chat_prompt - messages << OpenAI::ChatMessage.new(role: :assistant, content: prompt.payload.prompt) - messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have the following capabilities: #{prompt.payload.capabilities.to_json}") - messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have access to the following API: #{function_schemas(chat, prompt.payload.capabilities).to_json}") + if payload = chat_payload + messages << OpenAI::ChatMessage.new(role: :assistant, content: payload.prompt) + messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have the following capabilities: #{payload.capabilities.to_json}") + messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have access to the following API: #{function_schemas(chat, payload.capabilities).to_json}") messages << OpenAI::ChatMessage.new(role: :assistant, content: "If you were asked to perform any function of given capabilities, perform the action and reply with a confirmation telling what you have done.") messages.each { |m| save_history(chat.id.as(String), m) } @@ -144,10 +149,10 @@ module PlaceOS::Api messages end - private def driver_prompt(chat : PlaceOS::Model::Chat) : ChatPrompt? + private def driver_prompt(chat : PlaceOS::Model::Chat) : Payload? resp, code = exec_driver_func(chat, LLM_DRIVER, LLM_DRIVER_CHAT, nil) if code > 200 && code < 299 - ChatPrompt.new(message: "", payload: Payload.from_json(resp)) + Payload.from_json(resp) end end @@ -218,10 +223,6 @@ module PlaceOS::Api include JSON::Serializable end - record ChatPrompt, message : String, payload : Payload do - include JSON::Serializable - end - struct Payload include JSON::Serializable From 946f0a1f580dd6d92b8085af5e3d0979accab600 Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Tue, 17 Oct 2023 17:09:23 +1100 Subject: [PATCH 3/6] fix internals check --- shard.lock | 2 +- src/placeos-rest-api/controllers/chat_gpt.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.lock b/shard.lock index a8b11f5d..f6421628 100644 --- a/shard.lock +++ b/shard.lock @@ -199,7 +199,7 @@ shards: openssl_ext: git: https://github.com/spider-gazelle/openssl_ext.git - version: 2.3.0 + version: 2.4.4 opentelemetry-api: git: https://github.com/wyhaines/opentelemetry-api.cr.git diff --git a/src/placeos-rest-api/controllers/chat_gpt.cr b/src/placeos-rest-api/controllers/chat_gpt.cr index f10371c1..3480f7d7 100644 --- a/src/placeos-rest-api/controllers/chat_gpt.cr +++ b/src/placeos-rest-api/controllers/chat_gpt.cr @@ -67,7 +67,7 @@ module PlaceOS::Api record Config, api_key : String, api_base : String? protected def config - if internals = authority.internals["openai"] + if internals = authority.internals["openai"]? key = internals["api_key"]?.try &.as_s || Api::OPENAI_API_KEY || raise Error::NotFound.new("missing openai api_key configuration") Config.new(key, internals["api_base"]?.try &.as_s || Api::OPENAI_API_BASE) else From e47dea8c48a50323759ecfeb6e7bc1f7831e7e73 Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Tue, 17 Oct 2023 18:13:24 +1100 Subject: [PATCH 4/6] use gpt4 and make function discovery a two step process --- .../controllers/openai/chat_manager.cr | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/src/placeos-rest-api/controllers/openai/chat_manager.cr b/src/placeos-rest-api/controllers/openai/chat_manager.cr index b93ac90a..d44631ab 100644 --- a/src/placeos-rest-api/controllers/openai/chat_manager.cr +++ b/src/placeos-rest-api/controllers/openai/chat_manager.cr @@ -86,7 +86,7 @@ module PlaceOS::Api private def build_completion(messages, functions) OpenAI::ChatCompletionRequest.new( - model: OpenAI::GPT3Dot5Turbo, # gpt-3.5-turbo + model: OpenAI::GPT4, # required for competent use of functions messages: messages, functions: functions, function_call: "auto" @@ -125,9 +125,12 @@ module PlaceOS::Api if payload = chat_payload messages << OpenAI::ChatMessage.new(role: :assistant, content: payload.prompt) - messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have the following capabilities: #{payload.capabilities.to_json}") - messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have access to the following API: #{function_schemas(chat, payload.capabilities).to_json}") - messages << OpenAI::ChatMessage.new(role: :assistant, content: "If you were asked to perform any function of given capabilities, perform the action and reply with a confirmation telling what you have done.") + messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have the following capabilities:\n```json\n#{payload.capabilities.to_json}\n```\n" + + "if a request could benefit from these capabilities you can obtain a list of the functions by providing the relevent capability id\n" + + "then you can use any of the functions to perform actions on behalf of the user, make sure to reply with the results" + ) + # messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have access to the following API: #{function_schemas(chat, payload.capabilities).to_json}") + # messages << OpenAI::ChatMessage.new(role: :assistant, content: "If you were asked to perform any function of given capabilities, perform the action and reply with a confirmation telling what you have done.") messages.each { |m| save_history(chat.id.as(String), m) } else @@ -158,21 +161,39 @@ module PlaceOS::Api private def build_executor(chat) executor = OpenAI::FunctionExecutor.new + + executor.add( + name: "list_function_schemas", + description: "obtains the list of functions available for a capability", + clz: FunctionDiscovery) do |call| + request = call.as(FunctionDiscovery) + reply = "No response received" + begin + resp, code = exec_driver_func(chat, request.id, "function_schemas", [] of String) + reply = resp if 200 <= code <= 299 + rescue ex + Log.error(exception: ex) { {id: request.id, function: "function_schemas", args: request.id} } + reply = "Encountered error: #{ex.message}" + end + DriverResponse.new(reply).as(JSON::Serializable) + end + executor.add( - name: "call_driver_func", - description: "Executes functionality offered by driver", - clz: DriverExecutor) do |call| - request = call.as(DriverExecutor) + name: "call_function", + description: "Executes functionality offered by a capability", + clz: FunctionExecutor) do |call| + request = call.as(FunctionExecutor) reply = "No response received" begin - resp, code = exec_driver_func(chat, request.id, request.driver_func, request.args) + resp, code = exec_driver_func(chat, request.id, request.function, request.parameters) reply = resp if 200 <= code <= 299 rescue ex - Log.error(exception: ex) { {id: request.id, function: request.driver_func, args: request.args.to_s} } + Log.error(exception: ex) { {id: request.id, function: request.function, args: request.parameters.to_s} } reply = "Encountered error: #{ex.message}" end DriverResponse.new(reply).as(JSON::Serializable) end + executor end @@ -205,18 +226,26 @@ module PlaceOS::Api ) end - private struct DriverExecutor + private struct FunctionExecutor extend OpenAI::FuncMarker include JSON::Serializable - @[JSON::Field(description: "The ID of the driver which provides the functionality")] + @[JSON::Field(description: "The ID of the capability")] getter id : String - @[JSON::Field(description: "The name of the driver function which will be invoked to perform action. Value placeholders must be replaced with actual values")] - getter driver_func : String + @[JSON::Field(description: "The name of the function")] + getter function : String + + @[JSON::Field(description: "a JSON hash representing the named arguments of the function, as per the JSON schema provided")] + getter parameters : JSON::Any? + end - @[JSON::Field(description: "A string representation of the JSON that should be sent as the arguments to driver function")] - getter args : JSON::Any? + private struct FunctionDiscovery + extend OpenAI::FuncMarker + include JSON::Serializable + + @[JSON::Field(description: "The ID of the capability")] + getter id : String end private record DriverResponse, body : String do From fdea497f18efc937a9ebba563e84ba8bfc20ccfc Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Tue, 17 Oct 2023 20:11:12 +1100 Subject: [PATCH 5/6] fix prompt not being configured --- .../controllers/openai/chat_manager.cr | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/placeos-rest-api/controllers/openai/chat_manager.cr b/src/placeos-rest-api/controllers/openai/chat_manager.cr index d44631ab..d461f8b3 100644 --- a/src/placeos-rest-api/controllers/openai/chat_manager.cr +++ b/src/placeos-rest-api/controllers/openai/chat_manager.cr @@ -63,11 +63,15 @@ module PlaceOS::Api resp = openai_interaction(client, completion_req, executor, message, chat_id) ws.send(resp.to_json) end + rescue error + Log.warn(exception: error) { "failure processing chat message" } + ws.send({message: "error: #{error}"}.to_json) + ws.close end private def setup(chat, chat_payload) client = build_client - executor = build_executor(chat) + executor = build_executor(chat, chat_payload) chat_completion = build_completion(build_prompt(chat, chat_payload), executor.functions) {client, executor, chat_completion} @@ -124,13 +128,11 @@ module PlaceOS::Api messages = [] of OpenAI::ChatMessage if payload = chat_payload - messages << OpenAI::ChatMessage.new(role: :assistant, content: payload.prompt) - messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have the following capabilities:\n```json\n#{payload.capabilities.to_json}\n```\n" + - "if a request could benefit from these capabilities you can obtain a list of the functions by providing the relevent capability id\n" + - "then you can use any of the functions to perform actions on behalf of the user, make sure to reply with the results" - ) - # messages << OpenAI::ChatMessage.new(role: :assistant, content: "You have access to the following API: #{function_schemas(chat, payload.capabilities).to_json}") - # messages << OpenAI::ChatMessage.new(role: :assistant, content: "If you were asked to perform any function of given capabilities, perform the action and reply with a confirmation telling what you have done.") + messages << OpenAI::ChatMessage.new(role: :system, content: payload.prompt) + messages << OpenAI::ChatMessage.new(role: :system, content: "request function lists and call functions as required to fulfil requests.\n" + + "make sure to interpret and reply with the results of function calls.\n" + + "remember to only use valid capability ids, they can be found in this JSON:\n```json\n#{payload.capabilities.to_json}\n```" + + "id strings are case sensitive and must not be modified.") messages.each { |m| save_history(chat.id.as(String), m) } else @@ -154,25 +156,37 @@ module PlaceOS::Api private def driver_prompt(chat : PlaceOS::Model::Chat) : Payload? resp, code = exec_driver_func(chat, LLM_DRIVER, LLM_DRIVER_CHAT, nil) - if code > 200 && code < 299 + if code >= 200 && code <= 299 Payload.from_json(resp) + else + raise "error obtaining chat prompt: #{resp} (#{code})" end end - private def build_executor(chat) + private def build_executor(chat, payload : Payload?) executor = OpenAI::FunctionExecutor.new + description = if payload + "You have the following capability list, described in the following JSON:\n```json\n#{payload.capabilities.to_json}\n```\n" + + "if a request could benefit from these capabilities you can obtain the list of functions by providing the id string.\n" + + "id strings are case sensitive and must not be modified." + else + "if a request could benefit from a capability you can obtain the list of functions by providing the id string\n" + + "id strings are case sensitive and must not be modified." + end + executor.add( name: "list_function_schemas", - description: "obtains the list of functions available for a capability", - clz: FunctionDiscovery) do |call| + description: description, + clz: FunctionDiscovery + ) do |call| request = call.as(FunctionDiscovery) reply = "No response received" begin - resp, code = exec_driver_func(chat, request.id, "function_schemas", [] of String) + resp, code = exec_driver_func(chat, request.id, "function_schemas") reply = resp if 200 <= code <= 299 rescue ex - Log.error(exception: ex) { {id: request.id, function: "function_schemas", args: request.id} } + Log.error(exception: ex) { {id: request.id, function: "function_schemas"} } reply = "Encountered error: #{ex.message}" end DriverResponse.new(reply).as(JSON::Serializable) @@ -180,7 +194,7 @@ module PlaceOS::Api executor.add( name: "call_function", - description: "Executes functionality offered by a capability", + description: "Executes functionality offered by a capability, you'll first need to obtain the function schema to perform a request", clz: FunctionExecutor) do |call| request = call.as(FunctionExecutor) reply = "No response received" @@ -197,18 +211,7 @@ module PlaceOS::Api executor end - private def function_schemas(chat, capabilities) - schemas = Array(NamedTuple(function: String, description: String, parameters: Hash(String, JSON::Any))).new - capabilities.each do |capability| - resp, code = exec_driver_func(chat, capability.id, "function_schemas", nil) - if code > 200 && code < 299 - schemas += JSON.parse(resp).as_a - end - end - schemas - end - - private def exec_driver_func(chat, module_name, method, args) + private def exec_driver_func(chat, module_name, method, args = nil) remote_driver = RemoteDriver.new( sys_id: chat.system_id, module_name: module_name, @@ -230,7 +233,7 @@ module PlaceOS::Api extend OpenAI::FuncMarker include JSON::Serializable - @[JSON::Field(description: "The ID of the capability")] + @[JSON::Field(description: "The ID of the capability, exactly as provided in the capability list")] getter id : String @[JSON::Field(description: "The name of the function")] @@ -244,7 +247,7 @@ module PlaceOS::Api extend OpenAI::FuncMarker include JSON::Serializable - @[JSON::Field(description: "The ID of the capability")] + @[JSON::Field(description: "The ID of the capability, exactly as provided in the capability list")] getter id : String end From ebc5948d7cb30c57a9071a58fd38b1546a39c37c Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Wed, 18 Oct 2023 07:45:33 +1100 Subject: [PATCH 6/6] simplify prompts using fewer tokens --- src/placeos-rest-api/controllers/chat_gpt.cr | 2 +- .../{openai => chat_gpt}/chat_manager.cr | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) rename src/placeos-rest-api/controllers/{openai => chat_gpt}/chat_manager.cr (93%) diff --git a/src/placeos-rest-api/controllers/chat_gpt.cr b/src/placeos-rest-api/controllers/chat_gpt.cr index 3480f7d7..c261cc1b 100644 --- a/src/placeos-rest-api/controllers/chat_gpt.cr +++ b/src/placeos-rest-api/controllers/chat_gpt.cr @@ -78,4 +78,4 @@ module PlaceOS::Api end end -require "./openai/chat_manager" +require "./chat_gpt/chat_manager" diff --git a/src/placeos-rest-api/controllers/openai/chat_manager.cr b/src/placeos-rest-api/controllers/chat_gpt/chat_manager.cr similarity index 93% rename from src/placeos-rest-api/controllers/openai/chat_manager.cr rename to src/placeos-rest-api/controllers/chat_gpt/chat_manager.cr index d461f8b3..812dcf9e 100644 --- a/src/placeos-rest-api/controllers/openai/chat_manager.cr +++ b/src/placeos-rest-api/controllers/chat_gpt/chat_manager.cr @@ -130,9 +130,8 @@ module PlaceOS::Api if payload = chat_payload messages << OpenAI::ChatMessage.new(role: :system, content: payload.prompt) messages << OpenAI::ChatMessage.new(role: :system, content: "request function lists and call functions as required to fulfil requests.\n" + - "make sure to interpret and reply with the results of function calls.\n" + - "remember to only use valid capability ids, they can be found in this JSON:\n```json\n#{payload.capabilities.to_json}\n```" + - "id strings are case sensitive and must not be modified.") + "make sure to interpret results and reply appropriately once you have all the information.\n" + + "remember to only use valid capability ids, they can be found in this JSON:\n```json\n#{payload.capabilities.to_json}\n```") messages.each { |m| save_history(chat.id.as(String), m) } else @@ -168,11 +167,9 @@ module PlaceOS::Api description = if payload "You have the following capability list, described in the following JSON:\n```json\n#{payload.capabilities.to_json}\n```\n" + - "if a request could benefit from these capabilities you can obtain the list of functions by providing the id string.\n" + - "id strings are case sensitive and must not be modified." + "if a request could benefit from these capabilities, obtain the list of functions by providing the id string." else - "if a request could benefit from a capability you can obtain the list of functions by providing the id string\n" + - "id strings are case sensitive and must not be modified." + "if a request could benefit from a capability, obtain the list of functions by providing the id string" end executor.add( @@ -194,7 +191,7 @@ module PlaceOS::Api executor.add( name: "call_function", - description: "Executes functionality offered by a capability, you'll first need to obtain the function schema to perform a request", + description: "Executes functionality offered by a capability, you'll need to obtain the function schema to perform requests", clz: FunctionExecutor) do |call| request = call.as(FunctionExecutor) reply = "No response received"