Spaces:
Sleeping
Sleeping
WilliamGazeley
commited on
Commit
·
691fc98
1
Parent(s):
9e2a95f
Migrate to loguru
Browse files- .gitattributes +1 -0
- .gitignore +2 -0
- requirements.txt +1 -1
- src/app.py +0 -1
- src/functioncall.py +21 -19
- src/functions.py +6 -24
- src/logger.py +13 -0
- src/utils.py +5 -32
- src/validator.py +38 -21
.gitattributes
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.log
|
| 2 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -5,3 +5,5 @@ __pycache__/
|
|
| 5 |
|
| 6 |
# vLLM
|
| 7 |
inference_logs/
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
# vLLM
|
| 7 |
inference_logs/
|
| 8 |
+
|
| 9 |
+
logs/*
|
requirements.txt
CHANGED
|
@@ -25,4 +25,4 @@ langchain==0.1.9
|
|
| 25 |
accelerate==0.27.2
|
| 26 |
azure-search-documents==11.6.0b1
|
| 27 |
azure-identity==1.16.0
|
| 28 |
-
|
|
|
|
| 25 |
accelerate==0.27.2
|
| 26 |
azure-search-documents==11.6.0b1
|
| 27 |
azure-identity==1.16.0
|
| 28 |
+
loguru==0.7.2
|
src/app.py
CHANGED
|
@@ -3,7 +3,6 @@ from time import time
|
|
| 3 |
import huggingface_hub
|
| 4 |
import streamlit as st
|
| 5 |
from config import config
|
| 6 |
-
from utils import get_assistant_message
|
| 7 |
from functioncall import ModelInference
|
| 8 |
|
| 9 |
|
|
|
|
| 3 |
import huggingface_hub
|
| 4 |
import streamlit as st
|
| 5 |
from config import config
|
|
|
|
| 6 |
from functioncall import ModelInference
|
| 7 |
|
| 8 |
|
src/functioncall.py
CHANGED
|
@@ -3,21 +3,18 @@ import torch
|
|
| 3 |
import json
|
| 4 |
from config import config
|
| 5 |
from typing import List, Dict
|
|
|
|
| 6 |
|
| 7 |
-
from transformers import
|
| 8 |
-
AutoModelForCausalLM,
|
| 9 |
-
AutoTokenizer,
|
| 10 |
-
BitsAndBytesConfig
|
| 11 |
-
)
|
| 12 |
|
| 13 |
import functions
|
| 14 |
from prompter import PromptManager
|
| 15 |
from validator import validate_function_call_schema
|
| 16 |
from langchain_community.chat_models import ChatOllama
|
|
|
|
|
|
|
| 17 |
|
| 18 |
from utils import (
|
| 19 |
-
inference_logger,
|
| 20 |
-
get_assistant_message,
|
| 21 |
get_chat_template,
|
| 22 |
validate_and_extract_tool_calls
|
| 23 |
)
|
|
@@ -26,8 +23,9 @@ class ModelInference:
|
|
| 26 |
def __init__(self, chat_template: str):
|
| 27 |
self.prompter = PromptManager()
|
| 28 |
|
| 29 |
-
self.model = ChatOllama(model=config.ollama_model,
|
| 30 |
-
|
|
|
|
| 31 |
|
| 32 |
self.tokenizer = AutoTokenizer.from_pretrained(config.hf_model, trust_remote_code=True)
|
| 33 |
self.tokenizer.pad_token = self.tokenizer.eos_token
|
|
@@ -37,19 +35,22 @@ class ModelInference:
|
|
| 37 |
print("No chat template defined, getting chat_template...")
|
| 38 |
self.tokenizer.chat_template = get_chat_template(chat_template)
|
| 39 |
|
|
|
|
| 40 |
|
| 41 |
def process_completion_and_validate(self, completion, chat_template):
|
| 42 |
if completion:
|
|
|
|
|
|
|
| 43 |
validation, tool_calls, error_message = validate_and_extract_tool_calls(completion)
|
| 44 |
|
| 45 |
if validation:
|
| 46 |
-
|
| 47 |
return tool_calls, completion, error_message
|
| 48 |
else:
|
| 49 |
tool_calls = None
|
| 50 |
return tool_calls, completion, error_message
|
| 51 |
else:
|
| 52 |
-
|
| 53 |
raise ValueError("Assistant message is None")
|
| 54 |
|
| 55 |
def execute_function_call(self, tool_call):
|
|
@@ -58,7 +59,7 @@ class ModelInference:
|
|
| 58 |
function_to_call = getattr(functions, function_name, None)
|
| 59 |
function_args = tool_call.get("arguments", {})
|
| 60 |
|
| 61 |
-
|
| 62 |
function_response = function_to_call(*function_args.values())
|
| 63 |
results_dict = f'{{"name": "{function_name}", "content": {function_response}}}'
|
| 64 |
return results_dict
|
|
@@ -88,8 +89,9 @@ class ModelInference:
|
|
| 88 |
prompt.append({"role": "assistant", "content": assistant_message})
|
| 89 |
|
| 90 |
tool_message = f"Agent iteration {depth} to assist with user query: {query}\n"
|
|
|
|
| 91 |
if tool_calls:
|
| 92 |
-
|
| 93 |
|
| 94 |
for tool_call in tool_calls:
|
| 95 |
validation, message = validate_function_call_schema(tool_call, tools)
|
|
@@ -97,12 +99,12 @@ class ModelInference:
|
|
| 97 |
try:
|
| 98 |
function_response = self.execute_function_call(tool_call)
|
| 99 |
tool_message += f"<tool_response>\n{function_response}\n</tool_response>\n"
|
| 100 |
-
|
| 101 |
except Exception as e:
|
| 102 |
-
|
| 103 |
tool_message += f"<tool_response>\nThere was an error when executing the function: {tool_call.get('name')}\nHere's the error traceback: {e}\nPlease call this function again with correct arguments within XML tags <tool_call></tool_call>\n</tool_response>\n"
|
| 104 |
else:
|
| 105 |
-
|
| 106 |
tool_message += f"<tool_response>\nThere was an error validating function call against function signature: {tool_call.get('name')}\nHere's the error traceback: {message}\nPlease call this function again with correct arguments within XML tags <tool_call></tool_call>\n</tool_response>\n"
|
| 107 |
prompt.append({"role": "tool", "content": tool_message})
|
| 108 |
|
|
@@ -116,7 +118,7 @@ class ModelInference:
|
|
| 116 |
completion = self.run_inference(prompt)
|
| 117 |
return recursive_loop(prompt, completion, depth)
|
| 118 |
elif error_message:
|
| 119 |
-
|
| 120 |
tool_message += f"<tool_response>\nThere was an error parsing function calls\n Here's the error stack trace: {error_message}\nPlease call the function again with correct syntax<tool_response>"
|
| 121 |
prompt.append({"role": "tool", "content": tool_message})
|
| 122 |
|
|
@@ -128,11 +130,11 @@ class ModelInference:
|
|
| 128 |
completion = self.run_inference(prompt)
|
| 129 |
return recursive_loop(prompt, completion, depth)
|
| 130 |
else:
|
| 131 |
-
|
| 132 |
return assistant_message
|
| 133 |
|
| 134 |
return recursive_loop(prompt, completion, depth)
|
| 135 |
|
| 136 |
except Exception as e:
|
| 137 |
-
|
| 138 |
raise e
|
|
|
|
| 3 |
import json
|
| 4 |
from config import config
|
| 5 |
from typing import List, Dict
|
| 6 |
+
from logger import logger
|
| 7 |
|
| 8 |
+
from transformers import AutoTokenizer
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
import functions
|
| 11 |
from prompter import PromptManager
|
| 12 |
from validator import validate_function_call_schema
|
| 13 |
from langchain_community.chat_models import ChatOllama
|
| 14 |
+
from langchain.prompts import PromptTemplate
|
| 15 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 16 |
|
| 17 |
from utils import (
|
|
|
|
|
|
|
| 18 |
get_chat_template,
|
| 19 |
validate_and_extract_tool_calls
|
| 20 |
)
|
|
|
|
| 23 |
def __init__(self, chat_template: str):
|
| 24 |
self.prompter = PromptManager()
|
| 25 |
|
| 26 |
+
self.model = ChatOllama(model=config.ollama_model, temperature=0.0, format='json')
|
| 27 |
+
template = PromptTemplate(template="""<|im_start|>system\nYou are a function calling AI model. You are provided with function signatures within <tools></tools> XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: <tools> {"type": "function", "function": {"name": "get_stock_fundamentals", "description": "get_stock_fundamentals(symbol: str) -> dict - Get fundamental data for a given stock symbol using yfinance API.\\n\\n Args:\\n symbol (str): The stock symbol.\\n\\n Returns:\\n dict: A dictionary containing fundamental data.\\n Keys:\\n - \'symbol\': The stock symbol.\\n - \'company_name\': The long name of the company.\\n - \'sector\': The sector to which the company belongs.\\n - \'industry\': The industry to which the company belongs.\\n - \'market_cap\': The market capitalization of the company.\\n - \'pe_ratio\': The forward price-to-earnings ratio.\\n - \'pb_ratio\': The price-to-book ratio.\\n - \'dividend_yield\': The dividend yield.\\n - \'eps\': The trailing earnings per share.\\n - \'beta\': The beta value of the stock.\\n - \'52_week_high\': The 52-week high price of the stock.\\n - \'52_week_low\': The 52-week low price of the stock.", "parameters": {"type": "object", "properties": {"symbol": {"type": "string"}}, "required": ["symbol"]}}} </tools> Use the following pydantic model json schema for each tool call you will make: {"properties": {"arguments": {"title": "Arguments", "type": "object"}, "name": {"title": "Name", "type": "string"}}, "required": ["arguments", "name"], "title": "FunctionCall", "type": "object"} For each function call return a json object with function name and arguments within <tool_call></tool_call> XML tags as follows:\n<tool_call>\n{"arguments": <args-dict>, "name": <function-name>}\n</tool_call><|im_end|>\n""", input_variables=["question"])
|
| 28 |
+
chain = template | self.model | StrOutputParser()
|
| 29 |
|
| 30 |
self.tokenizer = AutoTokenizer.from_pretrained(config.hf_model, trust_remote_code=True)
|
| 31 |
self.tokenizer.pad_token = self.tokenizer.eos_token
|
|
|
|
| 35 |
print("No chat template defined, getting chat_template...")
|
| 36 |
self.tokenizer.chat_template = get_chat_template(chat_template)
|
| 37 |
|
| 38 |
+
logger.info(f"Model loaded: {self.model}")
|
| 39 |
|
| 40 |
def process_completion_and_validate(self, completion, chat_template):
|
| 41 |
if completion:
|
| 42 |
+
# completion = f"<tool_call>\n{completion}\n</tool_call>"
|
| 43 |
+
breakpoint()
|
| 44 |
validation, tool_calls, error_message = validate_and_extract_tool_calls(completion)
|
| 45 |
|
| 46 |
if validation:
|
| 47 |
+
logger.info(f"parsed tool calls:\n{json.dumps(tool_calls, indent=2)}")
|
| 48 |
return tool_calls, completion, error_message
|
| 49 |
else:
|
| 50 |
tool_calls = None
|
| 51 |
return tool_calls, completion, error_message
|
| 52 |
else:
|
| 53 |
+
logger.warning("Assistant message is None")
|
| 54 |
raise ValueError("Assistant message is None")
|
| 55 |
|
| 56 |
def execute_function_call(self, tool_call):
|
|
|
|
| 59 |
function_to_call = getattr(functions, function_name, None)
|
| 60 |
function_args = tool_call.get("arguments", {})
|
| 61 |
|
| 62 |
+
logger.info(f"Invoking function call {function_name} ...")
|
| 63 |
function_response = function_to_call(*function_args.values())
|
| 64 |
results_dict = f'{{"name": "{function_name}", "content": {function_response}}}'
|
| 65 |
return results_dict
|
|
|
|
| 89 |
prompt.append({"role": "assistant", "content": assistant_message})
|
| 90 |
|
| 91 |
tool_message = f"Agent iteration {depth} to assist with user query: {query}\n"
|
| 92 |
+
logger.info(f"Found tool calls: {tool_calls}")
|
| 93 |
if tool_calls:
|
| 94 |
+
logger.info(f"Assistant Message:\n{assistant_message}")
|
| 95 |
|
| 96 |
for tool_call in tool_calls:
|
| 97 |
validation, message = validate_function_call_schema(tool_call, tools)
|
|
|
|
| 99 |
try:
|
| 100 |
function_response = self.execute_function_call(tool_call)
|
| 101 |
tool_message += f"<tool_response>\n{function_response}\n</tool_response>\n"
|
| 102 |
+
logger.info(f"Here's the response from the function call: {tool_call.get('name')}\n{function_response}")
|
| 103 |
except Exception as e:
|
| 104 |
+
logger.info(f"Could not execute function: {e}")
|
| 105 |
tool_message += f"<tool_response>\nThere was an error when executing the function: {tool_call.get('name')}\nHere's the error traceback: {e}\nPlease call this function again with correct arguments within XML tags <tool_call></tool_call>\n</tool_response>\n"
|
| 106 |
else:
|
| 107 |
+
logger.info(message)
|
| 108 |
tool_message += f"<tool_response>\nThere was an error validating function call against function signature: {tool_call.get('name')}\nHere's the error traceback: {message}\nPlease call this function again with correct arguments within XML tags <tool_call></tool_call>\n</tool_response>\n"
|
| 109 |
prompt.append({"role": "tool", "content": tool_message})
|
| 110 |
|
|
|
|
| 118 |
completion = self.run_inference(prompt)
|
| 119 |
return recursive_loop(prompt, completion, depth)
|
| 120 |
elif error_message:
|
| 121 |
+
logger.info(f"Assistant Message:\n{assistant_message}")
|
| 122 |
tool_message += f"<tool_response>\nThere was an error parsing function calls\n Here's the error stack trace: {error_message}\nPlease call the function again with correct syntax<tool_response>"
|
| 123 |
prompt.append({"role": "tool", "content": tool_message})
|
| 124 |
|
|
|
|
| 130 |
completion = self.run_inference(prompt)
|
| 131 |
return recursive_loop(prompt, completion, depth)
|
| 132 |
else:
|
| 133 |
+
logger.info(f"Assistant Message:\n{assistant_message}")
|
| 134 |
return assistant_message
|
| 135 |
|
| 136 |
return recursive_loop(prompt, completion, depth)
|
| 137 |
|
| 138 |
except Exception as e:
|
| 139 |
+
logger.error(f"Exception occurred: {e}")
|
| 140 |
raise e
|
src/functions.py
CHANGED
|
@@ -8,7 +8,7 @@ from datetime import datetime
|
|
| 8 |
|
| 9 |
from typing import List
|
| 10 |
from bs4 import BeautifulSoup
|
| 11 |
-
from
|
| 12 |
from langchain.tools import tool
|
| 13 |
from langchain_core.utils.function_calling import convert_to_openai_tool
|
| 14 |
from config import config
|
|
@@ -69,13 +69,13 @@ def google_search_and_scrape(query: str) -> dict:
|
|
| 69 |
params = {'q': query, 'num': num_results}
|
| 70 |
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.3'}
|
| 71 |
|
| 72 |
-
|
| 73 |
response = requests.get(url, params=params, headers=headers)
|
| 74 |
soup = BeautifulSoup(response.text, 'html.parser')
|
| 75 |
urls = [result.find('a')['href'] for result in soup.find_all('div', class_='tF2Cxc')]
|
| 76 |
|
| 77 |
-
|
| 78 |
-
[
|
| 79 |
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
| 80 |
futures = [executor.submit(lambda url: (url, requests.get(url, headers=headers).text if isinstance(url, str) else None), url) for url in urls[:num_results] if isinstance(url, str)]
|
| 81 |
results = []
|
|
@@ -196,25 +196,6 @@ def get_key_financial_ratios(symbol: str) -> dict:
|
|
| 196 |
print(f"Error fetching key financial ratios for {symbol}: {e}")
|
| 197 |
return {}
|
| 198 |
|
| 199 |
-
@tool
|
| 200 |
-
def get_analyst_recommendations(symbol: str) -> pd.DataFrame:
|
| 201 |
-
"""
|
| 202 |
-
Get analyst recommendations for a given stock symbol.
|
| 203 |
-
|
| 204 |
-
Args:
|
| 205 |
-
symbol (str): The stock symbol.
|
| 206 |
-
|
| 207 |
-
Returns:
|
| 208 |
-
pd.DataFrame: DataFrame containing analyst recommendations.
|
| 209 |
-
"""
|
| 210 |
-
try:
|
| 211 |
-
stock = yf.Ticker(symbol)
|
| 212 |
-
recommendations = stock.recommendations
|
| 213 |
-
return recommendations
|
| 214 |
-
except Exception as e:
|
| 215 |
-
print(f"Error fetching analyst recommendations for {symbol}: {e}")
|
| 216 |
-
return pd.DataFrame()
|
| 217 |
-
|
| 218 |
@tool
|
| 219 |
def get_dividend_data(symbol: str) -> pd.DataFrame:
|
| 220 |
"""
|
|
@@ -245,6 +226,7 @@ def get_company_news(symbol: str) -> pd.DataFrame:
|
|
| 245 |
Returns:
|
| 246 |
pd.DataFrame: DataFrame containing company news and press releases.
|
| 247 |
"""
|
|
|
|
| 248 |
try:
|
| 249 |
news = yf.Ticker(symbol).news
|
| 250 |
return news
|
|
@@ -293,7 +275,7 @@ def get_openai_tools() -> List[dict]:
|
|
| 293 |
get_analysis,
|
| 294 |
# google_search_and_scrape,
|
| 295 |
get_current_stock_price,
|
| 296 |
-
|
| 297 |
# get_company_profile,
|
| 298 |
# get_stock_fundamentals,
|
| 299 |
# get_financial_statements,
|
|
|
|
| 8 |
|
| 9 |
from typing import List
|
| 10 |
from bs4 import BeautifulSoup
|
| 11 |
+
from logger import logger
|
| 12 |
from langchain.tools import tool
|
| 13 |
from langchain_core.utils.function_calling import convert_to_openai_tool
|
| 14 |
from config import config
|
|
|
|
| 69 |
params = {'q': query, 'num': num_results}
|
| 70 |
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.3'}
|
| 71 |
|
| 72 |
+
logger.info(f"Performing google search with query: {query}\nplease wait...")
|
| 73 |
response = requests.get(url, params=params, headers=headers)
|
| 74 |
soup = BeautifulSoup(response.text, 'html.parser')
|
| 75 |
urls = [result.find('a')['href'] for result in soup.find_all('div', class_='tF2Cxc')]
|
| 76 |
|
| 77 |
+
logger.info(f"Scraping text from urls, please wait...")
|
| 78 |
+
[logger.info(url) for url in urls]
|
| 79 |
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
| 80 |
futures = [executor.submit(lambda url: (url, requests.get(url, headers=headers).text if isinstance(url, str) else None), url) for url in urls[:num_results] if isinstance(url, str)]
|
| 81 |
results = []
|
|
|
|
| 196 |
print(f"Error fetching key financial ratios for {symbol}: {e}")
|
| 197 |
return {}
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
@tool
|
| 200 |
def get_dividend_data(symbol: str) -> pd.DataFrame:
|
| 201 |
"""
|
|
|
|
| 226 |
Returns:
|
| 227 |
pd.DataFrame: DataFrame containing company news and press releases.
|
| 228 |
"""
|
| 229 |
+
config.status.update(label=":newspaper: Getting news")
|
| 230 |
try:
|
| 231 |
news = yf.Ticker(symbol).news
|
| 232 |
return news
|
|
|
|
| 275 |
get_analysis,
|
| 276 |
# google_search_and_scrape,
|
| 277 |
get_current_stock_price,
|
| 278 |
+
get_company_news,
|
| 279 |
# get_company_profile,
|
| 280 |
# get_stock_fundamentals,
|
| 281 |
# get_financial_statements,
|
src/logger.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from time import time
|
| 3 |
+
from loguru import logger
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
log_dir = Path("logs")
|
| 7 |
+
log_dir.mkdir(exist_ok=True)
|
| 8 |
+
|
| 9 |
+
logger.remove() # Remove the default logger configuration
|
| 10 |
+
|
| 11 |
+
# Configure the logger to write logs to both files and stdout
|
| 12 |
+
logger.add(sys.stdout, format="{time} - {file} - {line} - {message}", backtrace=True)
|
| 13 |
+
logger.add(log_dir / f"{time()}.log", format="{time} - {file} - {line} - {message}", backtrace=True)
|
src/utils.py
CHANGED
|
@@ -5,6 +5,7 @@ import json
|
|
| 5 |
import logging
|
| 6 |
import datetime
|
| 7 |
import xml.etree.ElementTree as ET
|
|
|
|
| 8 |
|
| 9 |
from logging.handlers import RotatingFileHandler
|
| 10 |
|
|
@@ -27,9 +28,6 @@ file_handler.setLevel(logging.INFO)
|
|
| 27 |
formatter = logging.Formatter("%(asctime)s,%(msecs)03d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s", datefmt="%Y-%m-%d:%H:%M:%S")
|
| 28 |
file_handler.setFormatter(formatter)
|
| 29 |
|
| 30 |
-
inference_logger = logging.getLogger("function-calling-inference")
|
| 31 |
-
inference_logger.addHandler(file_handler)
|
| 32 |
-
|
| 33 |
def get_fewshot_examples(num_fewshot):
|
| 34 |
"""return a list of few shot examples"""
|
| 35 |
example_path = os.path.join(script_dir, 'prompt_assets', 'few_shot.json')
|
|
@@ -45,7 +43,7 @@ def get_chat_template(chat_template):
|
|
| 45 |
|
| 46 |
if not os.path.exists(template_path):
|
| 47 |
print
|
| 48 |
-
|
| 49 |
return None
|
| 50 |
try:
|
| 51 |
with open(template_path, 'r') as file:
|
|
@@ -55,31 +53,6 @@ def get_chat_template(chat_template):
|
|
| 55 |
print(f"Error loading template: {e}")
|
| 56 |
return None
|
| 57 |
|
| 58 |
-
def get_assistant_message(completion, chat_template, eos_token):
|
| 59 |
-
"""define and match pattern to find the assistant message"""
|
| 60 |
-
completion = completion.strip()
|
| 61 |
-
|
| 62 |
-
if chat_template == "zephyr":
|
| 63 |
-
assistant_pattern = re.compile(r'<\|assistant\|>((?:(?!<\|assistant\|>).)*)$', re.DOTALL)
|
| 64 |
-
elif chat_template == "chatml":
|
| 65 |
-
assistant_pattern = re.compile(r'<\|im_start\|>\s*assistant((?:(?!<\|im_start\|>\s*assistant).)*)$', re.DOTALL)
|
| 66 |
-
|
| 67 |
-
elif chat_template == "vicuna":
|
| 68 |
-
assistant_pattern = re.compile(r'ASSISTANT:\s*((?:(?!ASSISTANT:).)*)$', re.DOTALL)
|
| 69 |
-
else:
|
| 70 |
-
raise NotImplementedError(f"Handling for chat_template '{chat_template}' is not implemented.")
|
| 71 |
-
|
| 72 |
-
assistant_match = assistant_pattern.search(completion)
|
| 73 |
-
if assistant_match:
|
| 74 |
-
assistant_content = assistant_match.group(1).strip()
|
| 75 |
-
if chat_template == "vicuna":
|
| 76 |
-
eos_token = f"</s>{eos_token}"
|
| 77 |
-
return assistant_content.replace(eos_token, "")
|
| 78 |
-
else:
|
| 79 |
-
assistant_content = None
|
| 80 |
-
inference_logger.info("No match found for the assistant pattern")
|
| 81 |
-
return assistant_content
|
| 82 |
-
|
| 83 |
def validate_and_extract_tool_calls(assistant_content):
|
| 84 |
validation_result = False
|
| 85 |
tool_calls = []
|
|
@@ -108,11 +81,11 @@ def validate_and_extract_tool_calls(assistant_content):
|
|
| 108 |
f"- JSON Decode Error: {json_err}\n"\
|
| 109 |
f"- Fallback Syntax/Value Error: {eval_err}\n"\
|
| 110 |
f"- Problematic JSON text: {json_text}"
|
| 111 |
-
|
| 112 |
continue
|
| 113 |
except Exception as e:
|
| 114 |
error_message = f"Cannot strip text: {e}"
|
| 115 |
-
|
| 116 |
|
| 117 |
if json_data is not None:
|
| 118 |
tool_calls.append(json_data)
|
|
@@ -120,7 +93,7 @@ def validate_and_extract_tool_calls(assistant_content):
|
|
| 120 |
|
| 121 |
except ET.ParseError as err:
|
| 122 |
error_message = f"XML Parse Error: {err}"
|
| 123 |
-
|
| 124 |
|
| 125 |
# Return default values if no valid data is extracted
|
| 126 |
return validation_result, tool_calls, error_message
|
|
|
|
| 5 |
import logging
|
| 6 |
import datetime
|
| 7 |
import xml.etree.ElementTree as ET
|
| 8 |
+
from logger import logger
|
| 9 |
|
| 10 |
from logging.handlers import RotatingFileHandler
|
| 11 |
|
|
|
|
| 28 |
formatter = logging.Formatter("%(asctime)s,%(msecs)03d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s", datefmt="%Y-%m-%d:%H:%M:%S")
|
| 29 |
file_handler.setFormatter(formatter)
|
| 30 |
|
|
|
|
|
|
|
|
|
|
| 31 |
def get_fewshot_examples(num_fewshot):
|
| 32 |
"""return a list of few shot examples"""
|
| 33 |
example_path = os.path.join(script_dir, 'prompt_assets', 'few_shot.json')
|
|
|
|
| 43 |
|
| 44 |
if not os.path.exists(template_path):
|
| 45 |
print
|
| 46 |
+
logger.error(f"Template file not found: {chat_template}")
|
| 47 |
return None
|
| 48 |
try:
|
| 49 |
with open(template_path, 'r') as file:
|
|
|
|
| 53 |
print(f"Error loading template: {e}")
|
| 54 |
return None
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
def validate_and_extract_tool_calls(assistant_content):
|
| 57 |
validation_result = False
|
| 58 |
tool_calls = []
|
|
|
|
| 81 |
f"- JSON Decode Error: {json_err}\n"\
|
| 82 |
f"- Fallback Syntax/Value Error: {eval_err}\n"\
|
| 83 |
f"- Problematic JSON text: {json_text}"
|
| 84 |
+
logger.error(error_message)
|
| 85 |
continue
|
| 86 |
except Exception as e:
|
| 87 |
error_message = f"Cannot strip text: {e}"
|
| 88 |
+
logger.error(error_message)
|
| 89 |
|
| 90 |
if json_data is not None:
|
| 91 |
tool_calls.append(json_data)
|
|
|
|
| 93 |
|
| 94 |
except ET.ParseError as err:
|
| 95 |
error_message = f"XML Parse Error: {err}"
|
| 96 |
+
logger.error(f"XML Parse Error: {err}")
|
| 97 |
|
| 98 |
# Return default values if no valid data is extracted
|
| 99 |
return validation_result, tool_calls, error_message
|
src/validator.py
CHANGED
|
@@ -2,9 +2,11 @@ import ast
|
|
| 2 |
import json
|
| 3 |
from jsonschema import validate
|
| 4 |
from pydantic import ValidationError
|
| 5 |
-
from
|
|
|
|
| 6 |
from schema import FunctionCall, FunctionSignature
|
| 7 |
|
|
|
|
| 8 |
def validate_function_call_schema(call, signatures):
|
| 9 |
try:
|
| 10 |
call_data = FunctionCall(**call)
|
|
@@ -16,18 +18,26 @@ def validate_function_call_schema(call, signatures):
|
|
| 16 |
signature_data = FunctionSignature(**signature)
|
| 17 |
if signature_data.function.name == call_data.name:
|
| 18 |
# Validate types in function arguments
|
| 19 |
-
for arg_name, arg_schema in signature_data.function.parameters.get(
|
|
|
|
|
|
|
| 20 |
if arg_name in call_data.arguments:
|
| 21 |
call_arg_value = call_data.arguments[arg_name]
|
| 22 |
if call_arg_value:
|
| 23 |
try:
|
| 24 |
-
validate_argument_type(
|
|
|
|
|
|
|
| 25 |
except Exception as arg_validation_error:
|
| 26 |
return False, str(arg_validation_error)
|
| 27 |
|
| 28 |
# Check if all required arguments are present
|
| 29 |
-
required_arguments = signature_data.function.parameters.get(
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
if not result:
|
| 32 |
return False, f"Missing required arguments: {missing_arguments}"
|
| 33 |
|
|
@@ -39,21 +49,24 @@ def validate_function_call_schema(call, signatures):
|
|
| 39 |
# No matching function signature found
|
| 40 |
return False, f"No matching function signature found for function: {call_data.name}"
|
| 41 |
|
|
|
|
| 42 |
def check_required_arguments(call_arguments, required_arguments):
|
| 43 |
missing_arguments = [arg for arg in required_arguments if arg not in call_arguments]
|
| 44 |
return not bool(missing_arguments), missing_arguments
|
| 45 |
|
|
|
|
| 46 |
def validate_enum_value(arg_name, arg_value, enum_values):
|
| 47 |
if arg_value not in enum_values:
|
| 48 |
raise Exception(
|
| 49 |
f"Invalid value '{arg_value}' for parameter {arg_name}. Expected one of {', '.join(map(str, enum_values))}"
|
| 50 |
)
|
| 51 |
|
|
|
|
| 52 |
def validate_argument_type(arg_name, arg_value, arg_schema):
|
| 53 |
-
arg_type = arg_schema.get(
|
| 54 |
if arg_type:
|
| 55 |
-
if arg_type ==
|
| 56 |
-
enum_values = arg_schema[
|
| 57 |
if None not in enum_values and enum_values != []:
|
| 58 |
try:
|
| 59 |
validate_enum_value(arg_name, arg_value, enum_values)
|
|
@@ -63,20 +76,24 @@ def validate_argument_type(arg_name, arg_value, arg_schema):
|
|
| 63 |
|
| 64 |
python_type = get_python_type(arg_type)
|
| 65 |
if not isinstance(arg_value, python_type):
|
| 66 |
-
raise Exception(
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
def get_python_type(json_type):
|
| 69 |
type_mapping = {
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
}
|
| 78 |
return type_mapping[json_type]
|
| 79 |
|
|
|
|
| 80 |
def validate_json_data(json_object, json_schema):
|
| 81 |
valid = False
|
| 82 |
error_message = None
|
|
@@ -95,13 +112,13 @@ def validate_json_data(json_object, json_schema):
|
|
| 95 |
result_json = extract_json_from_markdown(json_object)
|
| 96 |
except Exception as e:
|
| 97 |
error_message = f"JSON decoding error: {e}"
|
| 98 |
-
|
| 99 |
return valid, result_json, error_message
|
| 100 |
|
| 101 |
# Return early if both json.loads and ast.literal_eval fail
|
| 102 |
if result_json is None:
|
| 103 |
error_message = "Failed to decode JSON data"
|
| 104 |
-
|
| 105 |
return valid, result_json, error_message
|
| 106 |
|
| 107 |
# Validate each item in the list against schema if it's a list
|
|
@@ -109,7 +126,7 @@ def validate_json_data(json_object, json_schema):
|
|
| 109 |
for index, item in enumerate(result_json):
|
| 110 |
try:
|
| 111 |
validate(instance=item, schema=json_schema)
|
| 112 |
-
|
| 113 |
except ValidationError as e:
|
| 114 |
error_message = f"Validation failed for item {index+1}: {e}"
|
| 115 |
break
|
|
@@ -125,8 +142,8 @@ def validate_json_data(json_object, json_schema):
|
|
| 125 |
|
| 126 |
if error_message is None:
|
| 127 |
valid = True
|
| 128 |
-
|
| 129 |
else:
|
| 130 |
-
|
| 131 |
|
| 132 |
return valid, result_json, error_message
|
|
|
|
| 2 |
import json
|
| 3 |
from jsonschema import validate
|
| 4 |
from pydantic import ValidationError
|
| 5 |
+
from logger import logger
|
| 6 |
+
from utils import extract_json_from_markdown
|
| 7 |
from schema import FunctionCall, FunctionSignature
|
| 8 |
|
| 9 |
+
|
| 10 |
def validate_function_call_schema(call, signatures):
|
| 11 |
try:
|
| 12 |
call_data = FunctionCall(**call)
|
|
|
|
| 18 |
signature_data = FunctionSignature(**signature)
|
| 19 |
if signature_data.function.name == call_data.name:
|
| 20 |
# Validate types in function arguments
|
| 21 |
+
for arg_name, arg_schema in signature_data.function.parameters.get(
|
| 22 |
+
"properties", {}
|
| 23 |
+
).items():
|
| 24 |
if arg_name in call_data.arguments:
|
| 25 |
call_arg_value = call_data.arguments[arg_name]
|
| 26 |
if call_arg_value:
|
| 27 |
try:
|
| 28 |
+
validate_argument_type(
|
| 29 |
+
arg_name, call_arg_value, arg_schema
|
| 30 |
+
)
|
| 31 |
except Exception as arg_validation_error:
|
| 32 |
return False, str(arg_validation_error)
|
| 33 |
|
| 34 |
# Check if all required arguments are present
|
| 35 |
+
required_arguments = signature_data.function.parameters.get(
|
| 36 |
+
"required", []
|
| 37 |
+
)
|
| 38 |
+
result, missing_arguments = check_required_arguments(
|
| 39 |
+
call_data.arguments, required_arguments
|
| 40 |
+
)
|
| 41 |
if not result:
|
| 42 |
return False, f"Missing required arguments: {missing_arguments}"
|
| 43 |
|
|
|
|
| 49 |
# No matching function signature found
|
| 50 |
return False, f"No matching function signature found for function: {call_data.name}"
|
| 51 |
|
| 52 |
+
|
| 53 |
def check_required_arguments(call_arguments, required_arguments):
|
| 54 |
missing_arguments = [arg for arg in required_arguments if arg not in call_arguments]
|
| 55 |
return not bool(missing_arguments), missing_arguments
|
| 56 |
|
| 57 |
+
|
| 58 |
def validate_enum_value(arg_name, arg_value, enum_values):
|
| 59 |
if arg_value not in enum_values:
|
| 60 |
raise Exception(
|
| 61 |
f"Invalid value '{arg_value}' for parameter {arg_name}. Expected one of {', '.join(map(str, enum_values))}"
|
| 62 |
)
|
| 63 |
|
| 64 |
+
|
| 65 |
def validate_argument_type(arg_name, arg_value, arg_schema):
|
| 66 |
+
arg_type = arg_schema.get("type", None)
|
| 67 |
if arg_type:
|
| 68 |
+
if arg_type == "string" and "enum" in arg_schema:
|
| 69 |
+
enum_values = arg_schema["enum"]
|
| 70 |
if None not in enum_values and enum_values != []:
|
| 71 |
try:
|
| 72 |
validate_enum_value(arg_name, arg_value, enum_values)
|
|
|
|
| 76 |
|
| 77 |
python_type = get_python_type(arg_type)
|
| 78 |
if not isinstance(arg_value, python_type):
|
| 79 |
+
raise Exception(
|
| 80 |
+
f"Type mismatch for parameter {arg_name}. Expected: {arg_type}, Got: {type(arg_value)}"
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
|
| 84 |
def get_python_type(json_type):
|
| 85 |
type_mapping = {
|
| 86 |
+
"string": str,
|
| 87 |
+
"number": (int, float),
|
| 88 |
+
"integer": int,
|
| 89 |
+
"boolean": bool,
|
| 90 |
+
"array": list,
|
| 91 |
+
"object": dict,
|
| 92 |
+
"null": type(None),
|
| 93 |
}
|
| 94 |
return type_mapping[json_type]
|
| 95 |
|
| 96 |
+
|
| 97 |
def validate_json_data(json_object, json_schema):
|
| 98 |
valid = False
|
| 99 |
error_message = None
|
|
|
|
| 112 |
result_json = extract_json_from_markdown(json_object)
|
| 113 |
except Exception as e:
|
| 114 |
error_message = f"JSON decoding error: {e}"
|
| 115 |
+
logger.info(f"Validation failed for JSON data: {error_message}")
|
| 116 |
return valid, result_json, error_message
|
| 117 |
|
| 118 |
# Return early if both json.loads and ast.literal_eval fail
|
| 119 |
if result_json is None:
|
| 120 |
error_message = "Failed to decode JSON data"
|
| 121 |
+
logger.info(f"Validation failed for JSON data: {error_message}")
|
| 122 |
return valid, result_json, error_message
|
| 123 |
|
| 124 |
# Validate each item in the list against schema if it's a list
|
|
|
|
| 126 |
for index, item in enumerate(result_json):
|
| 127 |
try:
|
| 128 |
validate(instance=item, schema=json_schema)
|
| 129 |
+
logger.info(f"Item {index+1} is valid against the schema.")
|
| 130 |
except ValidationError as e:
|
| 131 |
error_message = f"Validation failed for item {index+1}: {e}"
|
| 132 |
break
|
|
|
|
| 142 |
|
| 143 |
if error_message is None:
|
| 144 |
valid = True
|
| 145 |
+
logger.info("JSON data is valid against the schema.")
|
| 146 |
else:
|
| 147 |
+
logger.info(f"Validation failed for JSON data: {error_message}")
|
| 148 |
|
| 149 |
return valid, result_json, error_message
|