Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/node_modules
/venv
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn-error.log*
__pycache__/
30 changes: 30 additions & 0 deletions functions/getCoordinates.py
Original file line number Diff line number Diff line change
@@ -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())
25 changes: 25 additions & 0 deletions functions/getDogFacts.py
Original file line number Diff line number Diff line change
@@ -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())
28 changes: 28 additions & 0 deletions functions/getMatrixMul.py
Original file line number Diff line number Diff line change
@@ -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())
8 changes: 4 additions & 4 deletions functions/getWeather.ts
Original file line number Diff line number Diff line change
@@ -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;
}

/**
Expand Down
9 changes: 7 additions & 2 deletions package-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
48 changes: 48 additions & 0 deletions server/python/describe.py
Original file line number Diff line number Diff line change
@@ -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)
107 changes: 107 additions & 0 deletions server/python/descriptions.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
]
68 changes: 49 additions & 19 deletions server/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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);
}
Expand All @@ -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",
Expand Down
Loading