Skip to content

Commit 23e574f

Browse files
committed
initial agent and function parser
1 parent 8faa9ee commit 23e574f

File tree

4 files changed

+188
-0
lines changed

4 files changed

+188
-0
lines changed

funkagent/__init__.py

Whitespace-only changes.

funkagent/agents.py

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import json
2+
from typing import Optional
3+
from funkagent import parser
4+
5+
import openai
6+
7+
sys_msg = """Assistant is a large language model trained by OpenAI.
8+
9+
Assistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussion on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.
10+
11+
Assistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.
12+
13+
Overall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.
14+
"""
15+
16+
17+
class Agent:
18+
def __init__(
19+
self,
20+
openai_api_key: str,
21+
model_name: str = 'gpt-4-0613',
22+
functions: Optional[list] = None
23+
):
24+
openai.api_key = openai_api_key
25+
self.model_name = model_name
26+
self.functions = self._parse_functions(functions)
27+
self.func_mapping = self._create_func_mapping(functions)
28+
self.chat_history = [{'role': 'system', 'content': sys_msg}]
29+
30+
def _parse_functions(self, functions: Optional[list]) -> Optional[list]:
31+
if functions is None:
32+
return None
33+
return [parser.func_to_json(func) for func in functions]
34+
35+
def _create_func_mapping(self, functions: Optional[list]) -> dict:
36+
if functions is None:
37+
return {}
38+
return {func.__name__: func for func in functions}
39+
40+
def _create_chat_completion(
41+
self, messages: list, use_functions: bool=True
42+
) -> openai.ChatCompletion:
43+
if use_functions and self.functions:
44+
res = openai.ChatCompletion.create(
45+
model=self.model_name,
46+
messages=messages,
47+
functions=self.functions
48+
)
49+
else:
50+
res = openai.ChatCompletion.create(
51+
model=self.model_name,
52+
messages=messages
53+
)
54+
return res
55+
56+
def _generate_response(self) -> openai.ChatCompletion:
57+
while True:
58+
print('.', end='')
59+
res = self._create_chat_completion(
60+
self.chat_history + self.internal_thoughts
61+
)
62+
finish_reason = res.choices[0].finish_reason
63+
64+
if finish_reason == 'stop' or len(self.internal_thoughts) > 3:
65+
# create the final answer
66+
final_thought = self._final_thought_answer()
67+
final_res = self._create_chat_completion(
68+
self.chat_history + [final_thought],
69+
use_functions=False
70+
)
71+
return final_res
72+
elif finish_reason == 'function_call':
73+
self._handle_function_call(res)
74+
else:
75+
raise ValueError(f"Unexpected finish reason: {finish_reason}")
76+
77+
def _handle_function_call(self, res: openai.ChatCompletion):
78+
self.internal_thoughts.append(res.choices[0].message.to_dict())
79+
func_name = res.choices[0].message.function_call.name
80+
args_str = res.choices[0].message.function_call.arguments
81+
result = self._call_function(func_name, args_str)
82+
res_msg = {'role': 'assistant', 'content': (f"The answer is {result}.")}
83+
self.internal_thoughts.append(res_msg)
84+
85+
def _call_function(self, func_name: str, args_str: str):
86+
args = json.loads(args_str)
87+
func = self.func_mapping[func_name]
88+
res = func(**args)
89+
return res
90+
91+
def _final_thought_answer(self):
92+
thoughts = ("To answer the question I will use these step by step instructions."
93+
"\n\n")
94+
for thought in self.internal_thoughts:
95+
if 'function_call' in thought.keys():
96+
thoughts += (f"I will use the {thought['function_call']['name']} "
97+
"function to calculate the answer with arguments "
98+
+ thought['function_call']['arguments'] + ".\n\n")
99+
else:
100+
thoughts += thought["content"] + "\n\n"
101+
self.final_thought = {
102+
'role': 'assistant',
103+
'content': (f"{thoughts} Based on the above, I will now answer the "
104+
"question, this message will only be seen by me so answer with "
105+
"the assumption with that the user has not seen this message.")
106+
}
107+
return self.final_thought
108+
109+
def ask(self, query: str) -> openai.ChatCompletion:
110+
self.internal_thoughts = []
111+
self.chat_history.append({'role': 'user', 'content': query})
112+
res = self._generate_response()
113+
self.chat_history.append(res.choices[0].message.to_dict())
114+
return res

funkagent/parser.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import inspect
2+
import re
3+
4+
5+
def type_mapping(dtype):
6+
if dtype == float:
7+
return "number"
8+
elif dtype == int:
9+
return "integer"
10+
elif dtype == str:
11+
return "string"
12+
else:
13+
return "string"
14+
15+
def extract_params(doc_str: str):
16+
# parse the docstring to get the descriptions for each parameter in dict format
17+
params_str = doc_str.split("\n\n")[1].split("\n")
18+
params = {}
19+
for line in params_str:
20+
param_match = re.findall(r'(?<=:param )\w+(?=:)', line)
21+
if param_match != []:
22+
param_name = param_match[0]
23+
desc_match = line.replace(f":param {param_name}:", "").strip()
24+
params[param_name] = desc_match
25+
return params
26+
27+
def func_to_json(func):
28+
# first we get function name
29+
func_name = func.__name__
30+
# then we get the function annotations
31+
argspec = inspect.getfullargspec(func)
32+
# get the function docstring
33+
func_doc = inspect.getdoc(func)
34+
# parse the docstring to get the description
35+
func_description = func_doc.split("\n\n")[0]
36+
# get params
37+
params = argspec.annotations
38+
if 'return' in params.keys():
39+
del params['return']
40+
# parse the docstring to get the descriptions for each parameter in dict format
41+
param_details = extract_params(func_doc)
42+
# attach parameter types to params
43+
for param_name in argspec.args:
44+
params[param_name] = {
45+
"description": param_details.get(param_name) or "",
46+
"type": type_mapping(argspec.annotations[param_name])
47+
}
48+
# get parameters for function including default values (that are optional)
49+
len_optional_params = len(inspect.getfullargspec(func).defaults)
50+
# then return everything in dict
51+
return {
52+
"name": func_name,
53+
"description": func_description,
54+
"parameters": {
55+
"type": "object",
56+
"properties": params
57+
},
58+
"required": argspec.args[:len_optional_params]
59+
}

pyproject.toml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[tool.poetry]
2+
name = "funkagent"
3+
version = "0.0.1"
4+
description = "Minimal agent framework using OpenAI functions"
5+
authors = ["James Briggs <james@aurelio.ai>"]
6+
readme = "README.md"
7+
8+
[tool.poetry.dependencies]
9+
python = "^3.9"
10+
openai = "^0.27.8"
11+
12+
13+
[build-system]
14+
requires = ["poetry-core"]
15+
build-backend = "poetry.core.masonry.api"

0 commit comments

Comments
 (0)