10 changed files with 405 additions and 107 deletions
@ -1,2 +1,3 @@ |
|||||
/model/* |
/model/* |
||||
*.prof |
*.prof |
||||
|
__pycache__ |
@ -0,0 +1,8 @@ |
|||||
|
|
||||
|
print("running __main__.-py") |
||||
|
|
||||
|
from llama import main |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
main() |
@ -0,0 +1 @@ |
|||||
|
# empty |
@ -0,0 +1,7 @@ |
|||||
|
|
||||
|
|
||||
|
def tool_dummy(a: int, b: str): |
||||
|
return "result_%d_%s" % (a, b) |
||||
|
|
||||
|
def tool_dummy2(text: str): |
||||
|
return text.upper() |
@ -0,0 +1,30 @@ |
|||||
|
import pytest |
||||
|
import tool_helper |
||||
|
import tests.helper as helper |
||||
|
|
||||
|
|
||||
|
def test_tool_function_decorator_if_clean_tool_list(): |
||||
|
""" tests for the tool list to be empty. NOT strictly nessesary, |
||||
|
but I want to be warned if this is not the case anymore. Could be not the intention """ |
||||
|
start_len = len(tool_helper.tool_list) |
||||
|
assert start_len == 0 |
||||
|
|
||||
|
def test_tool_function_decorator(): |
||||
|
# get length before adding tools |
||||
|
start_len = len(tool_helper.tool_list) |
||||
|
|
||||
|
# add tools like it would be a decorator |
||||
|
tool_helper.tool(helper.tool_dummy) |
||||
|
tool_helper.tool(helper.tool_dummy2) |
||||
|
|
||||
|
# get length after adding tools |
||||
|
end_len = len(tool_helper.tool_list) |
||||
|
|
||||
|
# remove the added ones again |
||||
|
tool_helper.tool_list = tool_helper.tool_list[:-2] |
||||
|
|
||||
|
assert end_len == start_len + 2 |
||||
|
assert len(tool_helper.tool_list) == start_len |
||||
|
|
||||
|
|
||||
|
|
@ -0,0 +1,89 @@ |
|||||
|
import pytest |
||||
|
import tool_helper |
||||
|
from unittest import mock |
||||
|
import tests.helper as helper |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
def test_tool_dummy(): |
||||
|
with mock.patch("tests.helper.tool_dummy") as mock_dummy: |
||||
|
helper.tool_dummy() |
||||
|
mock_dummy.assert_called_once() # this will check if the mocked function on the context was called. |
||||
|
|
||||
|
|
||||
|
def test_tool_parse_no_exec(): |
||||
|
with mock.patch("tests.helper.tool_dummy") as mock_dummy: |
||||
|
tool_helper.parse_and_execute_tool_call("something else", [helper.tool_dummy, helper.tool_dummy2]) |
||||
|
assert mock_dummy.call_count == 0 |
||||
|
|
||||
|
|
||||
|
def test_match_and_extract_no_match(): |
||||
|
result = tool_helper._match_and_extract("something else", r"<tool_call>(.*)<\/tool_call>") |
||||
|
assert result == None |
||||
|
|
||||
|
|
||||
|
def test_match_and_extract_matching(): |
||||
|
result = tool_helper._match_and_extract("asdfsdfas <tool_call>{json content}</tool_call> adfafsd", r"<tool_call>(.*)<\/tool_call>") |
||||
|
assert result == "{json content}" |
||||
|
|
||||
|
|
||||
|
def test_match_and_extract_matching2(): |
||||
|
result = tool_helper._match_and_extract("<tool_call>{json content}</tool_call>", r"<tool_call>(.*)<\/tool_call>") |
||||
|
assert result == "{json content}" |
||||
|
|
||||
|
|
||||
|
def test_match_and_extract_matching3_with_newline(): |
||||
|
result = tool_helper._match_and_extract("<tool_call>\n{json content}\n</tool_call>", r"<tool_call>(.*)<\/tool_call>") |
||||
|
assert result == "\n{json content}\n" |
||||
|
|
||||
|
|
||||
|
def test_string_malformed_faulty(): |
||||
|
with mock.patch("utils.print_error") as print_error_mock: |
||||
|
result = tool_helper._execute_tool_call_str("{json_content}", []) |
||||
|
assert result == None |
||||
|
print_error_mock.assert_called_once() # this will check if the mocked function on the context was called. |
||||
|
|
||||
|
|
||||
|
def test_tool_call_json_1(): |
||||
|
with mock.patch("utils.print_error") as print_error_mock: |
||||
|
result = tool_helper._execute_tool_call_json({"name": "tool_dummy", "arguments": {"a": 1, "b": "zwei"}}, [helper.tool_dummy, helper.tool_dummy2]) |
||||
|
assert result == "result_1_zwei" |
||||
|
assert print_error_mock.call_count == 0 |
||||
|
|
||||
|
|
||||
|
def test_tool_call_json_2(): |
||||
|
with mock.patch("utils.print_error") as print_error_mock: |
||||
|
result = tool_helper._execute_tool_call_json({"name": "tool_dummy2", "arguments": {"text": "some_text"}}, [helper.tool_dummy, helper.tool_dummy2]) |
||||
|
assert result == "SOME_TEXT" |
||||
|
assert print_error_mock.call_count == 0 |
||||
|
|
||||
|
|
||||
|
def test_tool_call_json_non_existing_call_check(): |
||||
|
with mock.patch("utils.print_error") as print_error_mock: |
||||
|
result = tool_helper._execute_tool_call_json({"name": "tool_dummy_which_is_not_existing", "arguments": {"text": "some_text"}}, [helper.tool_dummy, helper.tool_dummy2]) |
||||
|
assert result == None |
||||
|
assert print_error_mock.call_count == 1 # this will check if the mocked function on the context was called. |
||||
|
|
||||
|
def test_tool_call_json_wrong_arguments_check(): |
||||
|
with mock.patch("utils.print_error") as print_error_mock: |
||||
|
result = tool_helper._execute_tool_call_json({"name": "tool_dummy", "arguments": {"a": "must_be_an_int_but_is_string", "b": "zwei"}}, [helper.tool_dummy, helper.tool_dummy2]) |
||||
|
assert result == None |
||||
|
assert print_error_mock.call_count == 1 # this will check if the mocked function on the context was called. |
||||
|
|
||||
|
|
||||
|
|
||||
|
def test_regex_multiline(): |
||||
|
import re |
||||
|
pattern = r"<start>(.*)</end>" |
||||
|
|
||||
|
# The text to search (spanning multiple lines) |
||||
|
text = """<start> |
||||
|
{json} |
||||
|
</end>""" |
||||
|
|
||||
|
# Use re.search with re.DOTALL to match across newlines |
||||
|
match = re.search(pattern, text, re.DOTALL) |
||||
|
|
||||
|
assert match.group(1).find("{json}") != -1 |
@ -0,0 +1,35 @@ |
|||||
|
import random |
||||
|
import datetime |
||||
|
from tool_helper import tool |
||||
|
|
||||
|
@tool |
||||
|
def current_time(): |
||||
|
"""Get the current local date and time as a string.""" |
||||
|
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M") |
||||
|
|
||||
|
# @tool |
||||
|
# def random_float(): |
||||
|
# """Generate a random float from 0..1.""" |
||||
|
# return str(random.random()) |
||||
|
|
||||
|
@tool |
||||
|
def random_float(a: float=0.0, b: float=1.0): |
||||
|
"""Generate a random float in range [a, b], including both end points. Optional pass no parameter and range 0..1 will be used. |
||||
|
Args: |
||||
|
a: minimum possible value |
||||
|
b: maximum possible value""" |
||||
|
return str(random.randint(a, b)) |
||||
|
|
||||
|
@tool |
||||
|
def random_int(a: int, b: int): |
||||
|
"""Generate a random integer in range [a, b], including both end points. |
||||
|
Args: |
||||
|
a: minimum possible value |
||||
|
b: maximum possible value""" |
||||
|
return str(random.randint(a, b)) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
def register_dummy(): |
||||
|
pass # dummy function to run and be sure the decorators have run |
@ -0,0 +1,102 @@ |
|||||
|
|
||||
|
from typing import Callable, List, Optional |
||||
|
import json |
||||
|
import re |
||||
|
import utils |
||||
|
|
||||
|
tool_list = [] |
||||
|
|
||||
|
|
||||
|
def tool(fn): |
||||
|
"""tool function decorator""" |
||||
|
print("register tool '%s'" % fn.__name__) |
||||
|
tool_list.append(fn) |
||||
|
|
||||
|
# def parse_and_execute_tool_call(message: str, tools: list[function]) -> str | None: |
||||
|
# """execute tool call if needed accordint <tool_call> tag and return the content of the tool call or None if no call happened.""" |
||||
|
# pass |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
def parse_and_execute_tool_call(message: str, tools: List[Callable]) -> Optional[str]: |
||||
|
""" |
||||
|
Execute a tool call if the <tool_call> tag is present and return the tool's response. |
||||
|
If no <tool_call> tag is found, return None. |
||||
|
|
||||
|
Args: |
||||
|
message (str): The message containing the tool call. |
||||
|
tools (list[function]): A list of tool functions available for execution. |
||||
|
|
||||
|
Returns: |
||||
|
Optional[str]: The content of the tool response or None if no tool call occurred. |
||||
|
""" |
||||
|
|
||||
|
# in case LLM responds with <tool_call></tool_call> the correct way |
||||
|
extracted = _match_and_extract(message, r"<tool_call>(.*)<\/tool_call>") |
||||
|
if extracted: |
||||
|
return _execute_tool_call_str(extracted, tools) |
||||
|
|
||||
|
# in case LLM responds with <tool_call></tool_response> by accident |
||||
|
extracted = _match_and_extract(message, r"<tool_call>(.*)<\/tool_.*>") |
||||
|
if extracted: |
||||
|
return _execute_tool_call_str(extracted, tools) |
||||
|
|
||||
|
# in case LLM responds with <tool_call></???> by accident |
||||
|
extracted = _match_and_extract(message, r"<tool_call>(.*)<\/.*>") |
||||
|
if extracted: |
||||
|
return _execute_tool_call_str(extracted, tools) |
||||
|
|
||||
|
# in case LLM responds with <tool_call></???> by accident |
||||
|
extracted = _match_and_extract(message, r"<tool_response>(.*)<\/.*>") |
||||
|
if extracted: |
||||
|
return _execute_tool_call_str(extracted, tools) |
||||
|
|
||||
|
return None |
||||
|
|
||||
|
|
||||
|
def _match_and_extract(message: str, pattern: str) -> Optional[str]: |
||||
|
""" helper function to match regex and extract group 1 """ |
||||
|
match = re.search(pattern, message, re.DOTALL) |
||||
|
if match: |
||||
|
group1 = match.group(1) |
||||
|
return group1 |
||||
|
return None |
||||
|
|
||||
|
|
||||
|
def _execute_tool_call_str(tool_call: str, tools: List[Callable]) -> Optional[str]: |
||||
|
""" execute tool call per string content. The content must be a valid json """ |
||||
|
try: |
||||
|
js = json.loads(tool_call) |
||||
|
return _execute_tool_call_json(js, tools) |
||||
|
except json.JSONDecodeError: |
||||
|
utils.print_error("Json was malformed. Will be ignored.") |
||||
|
return None |
||||
|
|
||||
|
def _execute_tool_call_json(data: any, tools: List[Callable]) -> Optional[str]: |
||||
|
""" extract name and arguments from parsed data and call the tool, which is matched from the tools list """ |
||||
|
# Extract tool name and arguments |
||||
|
tool_name = data.get("name") |
||||
|
arguments = data.get("arguments", {}) |
||||
|
|
||||
|
# Find the tool by name in the list of tools |
||||
|
for tool in tools: |
||||
|
if tool.__name__ == tool_name: |
||||
|
# Execute the tool |
||||
|
return _execute_tool_function(arguments, tool) |
||||
|
|
||||
|
utils.print_error("Specified tool '%s' not found." % tool_name) |
||||
|
return None |
||||
|
|
||||
|
def _execute_tool_function(arguments: any, tool: Callable) -> Optional[str]: |
||||
|
""" Execute the tool and return the result. """ |
||||
|
try: |
||||
|
result = tool(**arguments) |
||||
|
print("<tool_response>", result, "</tool_response>") |
||||
|
return result |
||||
|
except TypeError as e: |
||||
|
utils.print_error("Type error while executing function call: '%s'" % str(e)) |
||||
|
|
||||
|
return None |
@ -0,0 +1,11 @@ |
|||||
|
import json |
||||
|
import sys |
||||
|
|
||||
|
def load_json_file(filepath: str) -> any: |
||||
|
with open(filepath, "r") as f: |
||||
|
return json.load(f) |
||||
|
|
||||
|
|
||||
|
|
||||
|
def print_error(*args, **kwargs): |
||||
|
print(*args, file=sys.stderr, **kwargs) |
Loading…
Reference in new issue