diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..53c37a1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index d9bc2fc..ebc518d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,6 +17,7 @@ module.exports = { "react" ], "rules": { - "@typescript-eslint/no-extraneous-class": "off" + "@typescript-eslint/no-extraneous-class": "off", + "@typescript-eslint/strict-boolean-expressions": "off", } } diff --git a/package-lock.json b/package-lock.json index effce1c..5ad0b7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.33.2", - "react": "^18.2.0", "rollup": "^4.9.5", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-peer-deps-external": "^2.2.4", @@ -32,6 +31,10 @@ "ts-node": "^10.9.1", "tslib": "^2.6.2", "typescript": "^4.9.5" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4445,8 +4448,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -4675,7 +4677,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -8271,7 +8272,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8279,6 +8280,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -8680,6 +8694,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semantic-release": { "version": "23.0.0", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-23.0.0.tgz", diff --git a/package.json b/package.json index 99cb664..e21cb2b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.33.2", - "react": "^18.2.0", "rollup": "^4.9.5", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-peer-deps-external": "^2.2.4", @@ -49,6 +48,10 @@ "tslib": "^2.6.2", "typescript": "^4.9.5" }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, "keywords": [], "author": "", "license": "ISC", diff --git a/rollup.config.js b/rollup.config.js index 8c9c914..65a8348 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -27,8 +27,7 @@ export default [ commonjs(), typescript({ tsconfig: './tsconfig.json' }), terser() - ], - external: ['react', 'react-dom', 'styled-components'] + ] }, { input: 'src/index.ts', diff --git a/src/ui/copilot/index.tsx b/src/components/copilot/index.tsx similarity index 67% rename from src/ui/copilot/index.tsx rename to src/components/copilot/index.tsx index f596d75..015b68a 100644 --- a/src/ui/copilot/index.tsx +++ b/src/components/copilot/index.tsx @@ -1,6 +1,6 @@ import React from 'react' -export const CopilotUI : React.FC = () => { +export const CopilotUI: React.FC = () => { return (

CopilotUI

diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..f26f077 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1 @@ +export * from './copilot' diff --git a/src/context/application_context.tsx b/src/context/application_context.tsx new file mode 100644 index 0000000..63ed235 --- /dev/null +++ b/src/context/application_context.tsx @@ -0,0 +1,132 @@ +import React, { useEffect } from 'react' +import { type ChatCompletionMessageToolCall } from 'openai/resources/chat/completions' +import { AgentAPI, type AgentCallResponse, type AgentMessage, type ToolExecutionMessage } from '../agent_api' + +export interface ChatMessage { + content: string + role: 'user' | 'assistant' +} + +export interface PalicoContextProps { + loading: boolean + deploymentId: number + conversationHistory: ChatMessage[] + sendMessage: (message: string) => Promise +} + +export const PalicoContext = React.createContext({ + loading: false, + deploymentId: -1, + conversationHistory: [], + sendMessage: async () => {} +}) + +export type ToolHandler = (input: Input) => Promise + +export interface PalicoContextProviderProps { + tools: Record> + deploymentId: number + children?: any +} + +export const PalicoContextProvider: React.FC = ({ + deploymentId, + tools, + children +}) => { + const [loading, setLoading] = React.useState(false) + const [conversationId, setConversationId] = React.useState() + const [messageHistory, setMessageHistory] = React.useState([]) + // TODO: Convert to step-based pending message (create user reply -> handle user reply -> handle tool call -> end) + const [pendingMessage, setPendingMessage] = React.useState() + + useEffect(() => { + const callTool = async (tool: ChatCompletionMessageToolCall): Promise => { + const toolHandler = tools[tool.function.name] + if (toolHandler === null || toolHandler === undefined) { + throw new Error(`Tool ${tool.function.name} not found`) + } + const output = await toolHandler(JSON.parse(tool.function.arguments)) + return { + toolId: tool.id, + functionName: tool.function.name, + output + } + } + + const handleToolCall = async (message: AgentMessage, conversationId: number): Promise => { + if (!message.toolCalls) return + const toolCallResponse = await Promise.all(message.toolCalls.map(callTool)) + const response = await AgentAPI.replyToToolCall({ + deploymentId, + conversationId, + toolOutputs: toolCallResponse + }) + await handleAgentResponse(response, conversationId) + } + + const handleAgentResponse = async (response: AgentCallResponse, conversationId: number): Promise => { + if (response.message.toolCalls) { + await handleToolCall(response.message, conversationId) + } + if (response.message.content) { + setMessageHistory([ + ...messageHistory, + { + content: response.message.content.toString(), + role: 'assistant' + } + ]) + } + } + + const handlePendingMessage = async (): Promise => { + console.log('Handle pending message') + if (!pendingMessage) return + try { + setPendingMessage(undefined) + if (!conversationId) { + const response = await AgentAPI.newConversation({ + deploymentId, + message: pendingMessage + }) + setConversationId(response.conversationId) + await handleAgentResponse(response, response.conversationId) + } else { + const response = await AgentAPI.replyAsUser({ + deploymentId, + conversationId, + message: pendingMessage + }) + await handleAgentResponse(response, conversationId) + } + } catch (e) { + console.log(e) + } finally { + setLoading(false) + } + } + + void handlePendingMessage() + }, [conversationId, deploymentId, messageHistory, pendingMessage, tools]) + + const sendMessage = async (message: string): Promise => { + setLoading(true) + setPendingMessage(message) + setMessageHistory([ + ...messageHistory, + { + content: message, + role: 'user' + } + ]) + } + + return ( + + {children} + + ) +} diff --git a/src/context/index.ts b/src/context/index.ts new file mode 100644 index 0000000..2cbc10c --- /dev/null +++ b/src/context/index.ts @@ -0,0 +1 @@ +export * from './application_context' diff --git a/src/hello_world.tsx b/src/hello_world.tsx deleted file mode 100644 index ca305dd..0000000 --- a/src/hello_world.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const HelloWorld : React.FC = () => { - return
Hello World
; -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index eb9edcf..83b70af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -export * from './hello_world' -export * from './ui' +export * from './context' +export * from './components' export * from './agent_api' diff --git a/src/ui/index.ts b/src/ui/index.ts deleted file mode 100644 index 161e274..0000000 --- a/src/ui/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './copilot' \ No newline at end of file