diff --git a/.gitignore b/.gitignore index c4a487a..9d2eb84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ /node_modules +/venv .DS_Store .env npm-debug.log* yarn-debug.log* -yarn-error.log* \ No newline at end of file +yarn-error.log* +__pycache__/ \ No newline at end of file diff --git a/functions/getCoordinates.py b/functions/getCoordinates.py new file mode 100644 index 0000000..e231da6 --- /dev/null +++ b/functions/getCoordinates.py @@ -0,0 +1,30 @@ +from typing import Optional, TypedDict +import geocoder +import json +import sys +from pydantic import BaseModel + +class SymphonyRequest(BaseModel): + address: str # The IP address to get the coordinates of. Defaults to 'me'. + +class SymphonyResponse(BaseModel): + lat: float + lng: float + +def handler(request: SymphonyRequest) -> SymphonyResponse: + """ + Returns the latitude and longitude of the given IP address. + """ + address = request.address + + g = geocoder.ip(address) + lat, lng = g.latlng + + return SymphonyResponse(lat=lat, lng=lng) + + +if __name__ == "__main__": + args = json.loads(sys.argv[1]) + request = SymphonyRequest(address=args['address']) + response = handler(request) + print(response.json()) diff --git a/functions/getDogFacts.py b/functions/getDogFacts.py new file mode 100644 index 0000000..6b004da --- /dev/null +++ b/functions/getDogFacts.py @@ -0,0 +1,25 @@ +import requests +import json +import sys +from pydantic import BaseModel + +class SymphonyRequest(BaseModel): + pass # No input required for this function + +class SymphonyResponse(BaseModel): + facts: list # The list of dog facts + +def handler(request: SymphonyRequest) -> SymphonyResponse: + """ + Fetches dog facts from an API. + """ + response = requests.get('https://dog-api.kinduff.com/api/facts') + data = response.json() + + return SymphonyResponse(facts=data['facts']) + +if __name__ == "__main__": + args = json.loads(sys.argv[1]) + request = SymphonyRequest() + response = handler(request) + print(response.json()) \ No newline at end of file diff --git a/functions/getMatrixMul.py b/functions/getMatrixMul.py new file mode 100644 index 0000000..e4f85ab --- /dev/null +++ b/functions/getMatrixMul.py @@ -0,0 +1,28 @@ +from typing import List +from pydantic import BaseModel +import json +import sys + +class SymphonyRequest(BaseModel): + matrix1: List[List[float]] # The first matrix to be multiplied. + matrix2: List[List[float]] # The second matrix to be multiplied. + +class SymphonyResponse(BaseModel): + result: List[List[float]] # The result of the matrix multiplication. + +def handler(request: SymphonyRequest) -> SymphonyResponse: + """ + Multiplies two matrices. + """ + matrix1 = request.matrix1 + matrix2 = request.matrix2 + + result = [[sum(a*b for a, b in zip(X_row, Y_col)) for Y_col in zip(*matrix2)] for X_row in matrix1] + + return SymphonyResponse(result=result) + +if __name__ == "__main__": + args = json.loads(sys.argv[1]) + request = SymphonyRequest(matrix1=args['matrix1'], matrix2=args['matrix2']) + response = handler(request) + print(response.json()) \ No newline at end of file diff --git a/functions/getWeather.ts b/functions/getWeather.ts index fa48da3..5aae42f 100644 --- a/functions/getWeather.ts +++ b/functions/getWeather.ts @@ -1,12 +1,12 @@ import axios from "axios"; /** - * @property {string} lat Latitude of the city. - * @property {string} lon Longitude of the city. + * @property {number} lat Latitude of the city. + * @property {number} lon Longitude of the city. */ interface SymphonyRequest { - lat: string; - lon: string; + lat: number; + lon: number; } /** diff --git a/package-scripts.js b/package-scripts.js index 681ae30..29f972f 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -13,12 +13,17 @@ requiredEnvVariables.forEach((variable) => { process.exit(1); } }); + module.exports = { scripts: { - describe: "nodemon --watch functions --exec ts-node server/describe.ts", + describe: { + ts: "nodemon --watch functions --exec yarn ts-node server/typescript/describe.ts", + py: "nodemon --watch functions --exec python3 server/python/describe.py", + all: npsUtils.concurrent.nps("describe.ts", "describe.py"), + }, service: "ts-node server/service.ts", studio: "react-scripts start", - start: npsUtils.concurrent.nps("describe", "service", "studio"), + start: npsUtils.concurrent.nps("describe.all", "service", "studio"), clean: "yarn rimraf node_modules", }, }; diff --git a/package.json b/package.json index fba04e3..b1fb80d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "xstate": "^4.38.2" }, "devDependencies": { + "ts-node": "^10.9.1", "modularscale-sass": "^3.0.10", "nodemon": "^3.0.1", "typescript": "^5.2.2" diff --git a/server/python/describe.py b/server/python/describe.py new file mode 100644 index 0000000..edc35cd --- /dev/null +++ b/server/python/describe.py @@ -0,0 +1,48 @@ +import os +import json +from pydantic import BaseModel +from typing import Any, Callable, Type +import sys + +sys.path.insert(0, os.path.abspath('functions')) + +def generate_function_description(name, function: Callable[..., Any], request_model: Type[BaseModel], response_model: Type[BaseModel]) -> dict: + request_schema = request_model.model_json_schema() + response_schema = response_model.model_json_schema() + + # Remove the title field + request_schema.pop('title', None) + response_schema.pop('title', None) + + # Reorder the fields so that 'type' comes first + request_schema = {'type': request_schema['type'], **request_schema} + response_schema = {'type': response_schema['type'], **response_schema} + + + function_description = { + "name": name, + "description": function.__doc__, + "parameters": request_schema, + "returns": response_schema, + } + return function_description + +def main(directory): + descriptions = [] + for filename in os.listdir(directory): + if filename.endswith('.py'): + module_name = filename[:-3] + module = __import__(f'{module_name}') + function = getattr(module, 'handler') + symphony_request = getattr(module, 'SymphonyRequest') + symphony_response = getattr(module, 'SymphonyResponse') + fn_name = module_name + '-py' + description = generate_function_description(fn_name, function, symphony_request, symphony_response) + descriptions.append(description) + + with open('./server/python/descriptions.json', 'w') as f: + json.dump(descriptions, f, indent=4) + +if __name__ == '__main__': + directory = 'functions' + main(directory) diff --git a/server/python/descriptions.json b/server/python/descriptions.json new file mode 100644 index 0000000..993fb33 --- /dev/null +++ b/server/python/descriptions.json @@ -0,0 +1,107 @@ +[ + { + "name": "getDogFacts-py", + "description": "\n Fetches dog facts from an API.\n ", + "parameters": { + "type": "object", + "properties": {} + }, + "returns": { + "type": "object", + "properties": { + "facts": { + "items": {}, + "title": "Facts", + "type": "array" + } + }, + "required": [ + "facts" + ] + } + }, + { + "name": "getMatrixMul-py", + "description": "\n Multiplies two matrices.\n ", + "parameters": { + "type": "object", + "properties": { + "matrix1": { + "items": { + "items": { + "type": "number" + }, + "type": "array" + }, + "title": "Matrix1", + "type": "array" + }, + "matrix2": { + "items": { + "items": { + "type": "number" + }, + "type": "array" + }, + "title": "Matrix2", + "type": "array" + } + }, + "required": [ + "matrix1", + "matrix2" + ] + }, + "returns": { + "type": "object", + "properties": { + "result": { + "items": { + "items": { + "type": "number" + }, + "type": "array" + }, + "title": "Result", + "type": "array" + } + }, + "required": [ + "result" + ] + } + }, + { + "name": "getCoordinates-py", + "description": "\n Returns the latitude and longitude of the given IP address.\n ", + "parameters": { + "type": "object", + "properties": { + "address": { + "title": "Address", + "type": "string" + } + }, + "required": [ + "address" + ] + }, + "returns": { + "type": "object", + "properties": { + "lat": { + "title": "Lat", + "type": "number" + }, + "lng": { + "title": "Lng", + "type": "number" + } + }, + "required": [ + "lat", + "lng" + ] + } + } +] \ No newline at end of file diff --git a/server/service.ts b/server/service.ts index 280d562..3801f68 100644 --- a/server/service.ts +++ b/server/service.ts @@ -2,11 +2,13 @@ import { Server } from "ws"; import { createServer } from "http"; import { createMachine, interpret, EventObject, assign } from "xstate"; import OpenAI from "openai"; -import * as functionDescriptions from "./descriptions.json"; +import * as typescriptFunctions from "./typescript/descriptions.json"; +import * as pythonFunctions from "./python/descriptions.json"; import { pipe } from "fp-ts/lib/function"; import * as RAR from "fp-ts/ReadonlyArray"; import * as O from "fp-ts/Option"; import * as dotenv from "dotenv"; +import { exec } from "child_process"; dotenv.config(); @@ -61,25 +63,53 @@ const machine = createMachine( ); if (O.isSome(functionCall)) { - const name = functionCall.value.name; + const name = functionCall.value.name.replace("-", "."); const args = JSON.parse(functionCall.value.arguments); - import(`../functions/${name}.ts`) - .then(async (module) => { - const result = await module.default(args); - - const message = { - role: "function", - name, - content: JSON.stringify(result), - }; - - resolve(message); - }) - .catch((err) => { - console.error(`Failed to load function ${name}:`, err); - resolve(null); - }); + if (name.includes(".ts")) { + import(`../functions/${name}`) + .then(async (module) => { + const result = await module.default(args); + + const message = { + role: "function", + name: name.replace(".", "-"), + content: JSON.stringify(result), + }; + + resolve(message); + }) + .catch((err) => { + console.error(`Failed to load function ${name}:`, err); + resolve(null); + }); + } else if (name.includes(".py")) { + const pythonInterpreterPath = "venv/bin/python3"; + const pythonScriptPath = `functions/${name}`; + const argsString = JSON.stringify(args); + + exec( + `${pythonInterpreterPath} ${pythonScriptPath} '${argsString}'`, + (error, stdout) => { + if (error) { + console.error( + `Failed to execute python script ${name}:`, + error + ); + + resolve(null); + } else { + const message = { + role: "function", + name: name.replace(".", "-"), + content: stdout, + }; + + resolve(message); + } + } + ); + } } else { resolve(null); } @@ -102,7 +132,7 @@ const machine = createMachine( openai.chat.completions.create({ messages: [...context.messages, event.data], model: "gpt-4", - functions: functionDescriptions, + functions: [...typescriptFunctions, ...pythonFunctions], }), onDone: { target: "function", diff --git a/server/describe.ts b/server/typescript/describe.ts similarity index 82% rename from server/describe.ts rename to server/typescript/describe.ts index f17e485..3b66027 100644 --- a/server/describe.ts +++ b/server/typescript/describe.ts @@ -47,7 +47,7 @@ function generateMetadata(sourceFile: ts.SourceFile, fileName: string) { } }); - metadata.name = fileName.replace(".ts", ""); + metadata.name = fileName.replace(".", "-"); return metadata; } @@ -100,21 +100,23 @@ const readFiles = new Promise((resolve, reject) => { reject(error); } - files.forEach((fileName) => { - const code = fs.readFileSync( - `${FUNCTIONS_DIRECTORY}/${fileName}`, - "utf8" - ); - const sourceFile = ts.createSourceFile( - "temp.ts", - code, - ts.ScriptTarget.Latest, - true - ); - - const metadata = generateMetadata(sourceFile, fileName); - metadatas.push(metadata); - }); + files + .filter((fileName) => fileName.endsWith(".ts")) + .forEach((fileName) => { + const code = fs.readFileSync( + `${FUNCTIONS_DIRECTORY}/${fileName}`, + "utf8" + ); + const sourceFile = ts.createSourceFile( + "temp.ts", + code, + ts.ScriptTarget.Latest, + true + ); + + const metadata = generateMetadata(sourceFile, fileName); + metadatas.push(metadata); + }); resolve(metadatas); }); @@ -123,7 +125,7 @@ const readFiles = new Promise((resolve, reject) => { readFiles .then((metadatas) => { fs.writeFileSync( - "./server/descriptions.json", + "./server/typescript/descriptions.json", JSON.stringify(metadatas, null, 2) ); }) diff --git a/server/descriptions.json b/server/typescript/descriptions.json similarity index 84% rename from server/descriptions.json rename to server/typescript/descriptions.json index 04bd62d..4edabc5 100644 --- a/server/descriptions.json +++ b/server/typescript/descriptions.json @@ -1,16 +1,16 @@ [ { - "name": "getWeather", + "name": "getWeather-ts", "description": "Gets temperature of a city.", "parameters": { "type": "object", "properties": { "lat": { - "type": "string", + "type": "number", "description": "Latitude of the city." }, "lon": { - "type": "string", + "type": "number", "description": "Longitude of the city." } }, @@ -21,7 +21,7 @@ } }, { - "name": "kelvinToCelsius", + "name": "kelvinToCelsius-ts", "description": "Converts Kelvin to Celsius.", "parameters": { "type": "object", diff --git a/src/index.js b/src/index.js index 05f949b..4770096 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,10 @@ const Message = ({ message }) => {
{message.function_call ? (
{JSON.stringify(
JSON.parse(message.function_call.arguments),
@@ -32,6 +35,17 @@ const Message = ({ message }) => {
);
};
+const ChainOfThought = ({ messages }) => {
+ return (
+
+ Chain of Thought
+ {messages.map((message, index) => (
+
+ ))}
+
+ );
+};
+
const App = () => {
const socketRef = useRef(null);
const [messages, setMessages] = useState([]);
@@ -83,10 +97,33 @@ const App = () => {