diff --git a/.github/workflows/code_checks.yml b/.github/workflows/code_checks.yml index 776b4e8..887f59e 100644 --- a/.github/workflows/code_checks.yml +++ b/.github/workflows/code_checks.yml @@ -54,5 +54,10 @@ jobs: uses: pypa/gh-action-pip-audit@1220774d901786e6f652ae159f7b6bc8fea6d266 with: virtual-environment: .venv/ + # Skipping one nbconvert vulnerability that has no fix version + # Skipping one orjson vulnerability that has no fix version + # Skipping one protobuf vulnerability that has no fix version ignore-vulns: | GHSA-xm59-rqc7-hhvf + GHSA-hx9q-6w63-j58v + GHSA-7gcm-g887-7qv7 diff --git a/.gitignore b/.gitignore index ad52a41..78c1eae 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ wheels/ **.ipynb_checkpoints .env +.gradio +aieng-eval-agents/aieng/agent_evals/report_generation/data/*.db +aieng-eval-agents/aieng/agent_evals/report_generation/reports/* diff --git a/aieng-eval-agents/aieng/agent_evals/async_client_manager.py b/aieng-eval-agents/aieng/agent_evals/async_client_manager.py new file mode 100644 index 0000000..7d79b4b --- /dev/null +++ b/aieng-eval-agents/aieng/agent_evals/async_client_manager.py @@ -0,0 +1,193 @@ +"""Async client lifecycle manager for Gradio applications. + +Provides idempotent initialization and proper cleanup of async clients +like Weaviate and OpenAI to prevent event loop conflicts during Gradio's +hot-reload process. +""" + +import os +import sqlite3 +import urllib.parse +from pathlib import Path +from typing import Any + +import pandas as pd +from aieng.agent_evals.configs import Configs +from openai import AsyncOpenAI +from weaviate.client import WeaviateAsyncClient + + +# Will use these as default if no path is provided in the +# REPORT_GENERATION_DB_PATH and REPORTS_OUTPUT_PATH env vars +DEFAULT_SQLITE_DB_PATH = Path("aieng-eval-agents/aieng/agent_evals/report_generation/data/OnlineRetail.db") +DEFAULT_REPORTS_OUTPUT_PATH = Path("aieng-eval-agents/aieng/agent_evals/report_generation/reports/") + + +class SQLiteConnection: + """SQLite connection.""" + + def __init__(self) -> None: + db_path = os.getenv("REPORT_GENERATION_DB_PATH", DEFAULT_SQLITE_DB_PATH) + self._connection = sqlite3.connect(db_path) + + def execute(self, query: str) -> list[Any]: + """Execute a SQLite query. + + Args: + query: The SQLite query to execute. + + Returns + ------- + The result of the query. Will return the result of + `execute(query).fetchall()`. + """ + return self._connection.execute(query).fetchall() + + def close(self) -> None: + """Close the SQLite connection.""" + self._connection.close() + + +class ReportFileWriter: + """Write reports to a file.""" + + def write_report_to_file( + self, + report_data: list[Any], + report_columns: list[str], + filename: str = "report.xlsx", + gradio_link: bool = True, + ) -> str: + """Write a report to a XLSX file. + + Args: + report_data: The data of the report + report_columns: The columns of the report + filename: The name of the file to create. Default is "report.xlsx". + gradio_link: Whether to return a file link that works with Gradio UI. + Default is True. + + Returns + ------- + The path to the report file. If `gradio_link` is True, will return + a URL link that allows Gradio UI to donwload the file. + """ + # Create reports directory if it doesn't exist + reports_output_path = self.get_reports_output_path() + reports_output_path.mkdir(exist_ok=True) + filepath = reports_output_path / filename + + report_df = pd.DataFrame(report_data, columns=report_columns) + report_df.to_excel(filepath, index=False) + + file_uri = str(filepath) + if gradio_link: + file_uri = f"gradio_api/file={urllib.parse.quote(str(file_uri), safe='')}" + + return file_uri + + @staticmethod + def get_reports_output_path() -> Path: + """Get the reports output path. + + If no path is provided in the REPORTS_OUTPUT_PATH env var, will use the + default path in DEFAULT_REPORTS_OUTPUT_PATH. + + Returns + ------- + The reports output path. + """ + return Path(os.getenv("REPORTS_OUTPUT_PATH", DEFAULT_REPORTS_OUTPUT_PATH)) + + +class AsyncClientManager: + """Manages async client lifecycle with lazy initialization and cleanup. + + This class ensures clients are created only once and properly closed, + preventing ResourceWarning errors from unclosed event loops. + + Parameters + ---------- + configs: Configs | None, optional, default=None + Configuration object for client setup. If None, a new ``Configs()`` is created. + + Examples + -------- + >>> manager = AsyncClientManager() + >>> # Access clients (created on first access) + >>> weaviate = manager.weaviate_client + >>> kb = manager.knowledgebase + >>> openai = manager.openai_client + >>> # In finally block or cleanup + >>> await manager.close() + """ + + _singleton_instance: "AsyncClientManager | None" = None + + @classmethod + def get_instance(cls) -> "AsyncClientManager": + """Get the singleton instance of the client manager. + + Returns + ------- + The singleton instance of the client manager. + """ + if cls._singleton_instance is None: + cls._singleton_instance = AsyncClientManager() + return cls._singleton_instance + + def __init__(self, configs: Configs | None = None) -> None: + """Initialize manager with optional configs.""" + self._configs: Configs | None = configs + self._weaviate_client: WeaviateAsyncClient | None = None + self._openai_client: AsyncOpenAI | None = None + self._sqlite_connection: SQLiteConnection | None = None + self._report_file_writer: ReportFileWriter | None = None + self._initialized: bool = False + + @property + def configs(self) -> Configs: + """Get or create configs instance.""" + if self._configs is None: + self._configs = Configs() # pyright: ignore[reportCallIssue] + return self._configs + + @property + def openai_client(self) -> AsyncOpenAI: + """Get or create OpenAI client.""" + if self._openai_client is None: + self._openai_client = AsyncOpenAI() + self._initialized = True + return self._openai_client + + @property + def sqlite_connection(self) -> SQLiteConnection: + """Get or create SQLite session.""" + if self._sqlite_connection is None: + self._sqlite_connection = SQLiteConnection() + self._initialized = True + return self._sqlite_connection + + @property + def report_file_writer(self) -> ReportFileWriter: + """Get or create ReportFileWriter.""" + if self._report_file_writer is None: + self._report_file_writer = ReportFileWriter() + self._initialized = True + return self._report_file_writer + + async def close(self) -> None: + """Close all initialized async clients.""" + if self._openai_client is not None: + await self._openai_client.close() + self._openai_client = None + + if self._sqlite_connection is not None: + self._sqlite_connection.close() + self._sqlite_connection = None + + self._initialized = False + + def is_initialized(self) -> bool: + """Check if any clients have been initialized.""" + return self._initialized diff --git a/aieng-eval-agents/aieng/agent_evals/configs.py b/aieng-eval-agents/aieng/agent_evals/configs.py new file mode 100644 index 0000000..c3cc425 --- /dev/null +++ b/aieng-eval-agents/aieng/agent_evals/configs.py @@ -0,0 +1,109 @@ +"""Configuration settings for the agent evals.""" + +from pydantic import AliasChoices, Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Configs(BaseSettings): + """Configuration settings loaded from environment variables. + + This class automatically loads configuration values from environment variables + and a .env file, and provides type-safe access to all settings. It validates + environment variables on instantiation. + + Attributes + ---------- + openai_base_url : str + Base URL for OpenAI-compatible API (defaults to Gemini endpoint). + openai_api_key : str + API key for OpenAI-compatible API (accepts OPENAI_API_KEY, GEMINI_API_KEY, + or GOOGLE_API_KEY). + default_planner_model : str, default='gemini-2.5-pro' + Model name for planning tasks. This is typically a more capable and expensive + model. + default_worker_model : str, default='gemini-2.5-flash' + Model name for worker tasks. This is typically a less expensive model. + embedding_base_url : str + Base URL for embedding API service. + embedding_api_key : str + API key for embedding service. + embedding_model_name : str, default='@cf/baai/bge-m3' + Name of the embedding model. + weaviate_collection_name : str, default='enwiki_20250520' + Name of the Weaviate collection to use. + weaviate_api_key : str + API key for Weaviate cloud instance. + weaviate_http_host : str + Weaviate HTTP host (must end with .weaviate.cloud). + weaviate_grpc_host : str + Weaviate gRPC host (must start with grpc- and end with .weaviate.cloud). + weaviate_http_port : int, default=443 + Port for Weaviate HTTP connections. + weaviate_grpc_port : int, default=443 + Port for Weaviate gRPC connections. + weaviate_http_secure : bool, default=True + Use secure HTTP connection. + weaviate_grpc_secure : bool, default=True + Use secure gRPC connection. + langfuse_public_key : str + Langfuse public key (must start with pk-lf-). + langfuse_secret_key : str + Langfuse secret key (must start with sk-lf-). + langfuse_host : str, default='https://us.cloud.langfuse.com' + Langfuse host URL. + e2b_api_key : str or None + Optional E2B.dev API key for code interpreter (must start with e2b_). + default_code_interpreter_template : str or None + Optional default template name or ID for E2B.dev code interpreter. + web_search_base_url : str or None + Optional base URL for web search service. + web_search_api_key : str or None + Optional API key for web search service. + + Examples + -------- + >>> from src.utils.env_vars import Configs + >>> config = Configs() + >>> print(config.default_planner_model) + 'gemini-2.5-pro' + + Notes + ----- + Create a .env file in your project root with the required environment + variables. The class will automatically load and validate them. + """ + + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", env_ignore_empty=True) + + openai_base_url: str = "https://generativelanguage.googleapis.com/v1beta/openai/" + openai_api_key: str = Field(validation_alias=AliasChoices("OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY")) + + default_planner_model: str = "gemini-2.5-pro" + default_worker_model: str = "gemini-2.5-flash" + + embedding_base_url: str + embedding_api_key: str + embedding_model_name: str = "@cf/baai/bge-m3" + + weaviate_collection_name: str = "enwiki_20250520" + weaviate_api_key: str | None = None + # ends with .weaviate.cloud, or it's "localhost" + weaviate_http_host: str = Field(pattern=r"^.*\.weaviate\.cloud$|localhost") + # starts with grpc- ends with .weaviate.cloud, or it's "localhost" + weaviate_grpc_host: str = Field(pattern=r"^grpc-.*\.weaviate\.cloud$|localhost") + weaviate_http_port: int = 443 + weaviate_grpc_port: int = 443 + weaviate_http_secure: bool = True + weaviate_grpc_secure: bool = True + + langfuse_public_key: str = Field(pattern=r"^pk-lf-.*$") + langfuse_secret_key: str = Field(pattern=r"^sk-lf-.*$") + langfuse_host: str = "https://us.cloud.langfuse.com" + + # Optional E2B.dev API key for Python Code Interpreter tool + e2b_api_key: str | None = Field(default=None, pattern=r"^e2b_.*$") + default_code_interpreter_template: str | None = "9p6favrrqijhasgkq1tv" + + # Optional configs for web search tool + web_search_base_url: str | None = None + web_search_api_key: str | None = None diff --git a/aieng-eval-agents/aieng/agent_evals/report_generation/README.md b/aieng-eval-agents/aieng/agent_evals/report_generation/README.md new file mode 100644 index 0000000..7af8aa0 --- /dev/null +++ b/aieng-eval-agents/aieng/agent_evals/report_generation/README.md @@ -0,0 +1,52 @@ +# Report Generation Agent + +This code implements an example of a Report Generation Agent for single-table relational +data source. + +The data source implemented here is [SQLite](https://sqlite.org/) which is supported +natively by Python and saves the data in disk. + +The Report Generation Agent will provide an UI to read user queries in natural language +and procceed to make SQL queries to the database in order to produce the data for +the report. At the end, the Agent will provide a downloadable link to the report as +an `.xlsx` file. + +## Dataset + +The dataset used in this example is the +[Online Retail](https://archive.ics.uci.edu/dataset/352/online+retail) dataset. It contains +information about invoices for products that were purchased by customers, which also includes +product quantity, the invoice date and country that the user resides in. For a more +detailed data structure, please check the [OnlineRetail.ddl](data/Online%20Retail.ddl) file. + +## Importing the Data + +To import the data, pleasde download the dataset file from the link below and save it to your +file system. + +https://archive.ics.uci.edu/static/public/352/online+retail.zip + +You can import the dataset to the database by running the script below: + +```bash +uv run --env-file .env python -m aieng.agent_evals.report_generation.data.import_online_retail_data --dataset-path +``` + +Replace `` with the path the dataset's .CSV file is saved in your machine. + +***NOTE:*** You can configure the location the database is saved by setting the path to +an environment variable named `REPORT_GENERATION_DB_PATH`. + +## Running + +To run the agent, please execute: + +```bash +uv run --env-file .env python -m aieng.agent_evals.report_generation.main +``` + +The agent will be available through a [Gradio](https://www.gradio.app/) web UI under the +local address http://127.0.0.1:7860, which can be accessed on your preferred browser. + +On the UI, there will be a few examples of requests you can make to this agent. It also +features a text input so you can make your own report requests to it. diff --git a/aieng-eval-agents/aieng/agent_evals/report_generation/data/OnlineRetail.ddl b/aieng-eval-agents/aieng/agent_evals/report_generation/data/OnlineRetail.ddl new file mode 100644 index 0000000..fab35a3 --- /dev/null +++ b/aieng-eval-agents/aieng/agent_evals/report_generation/data/OnlineRetail.ddl @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS "sales" ( + "InvoiceNo" INTEGER, + "StockCode" TEXT, + "Description" TEXT, + "Quantity" INTEGER, + "InvoiceDate" TEXT, + "UnitPrice" REAL, + "CustomerID" INTEGER, + "Country" TEXT +); diff --git a/aieng-eval-agents/aieng/agent_evals/report_generation/data/import_online_retail_data.py b/aieng-eval-agents/aieng/agent_evals/report_generation/data/import_online_retail_data.py new file mode 100644 index 0000000..69e7832 --- /dev/null +++ b/aieng-eval-agents/aieng/agent_evals/report_generation/data/import_online_retail_data.py @@ -0,0 +1,99 @@ +"""Import the Online Retail dataset to a SQLite database.""" + +import logging +import os +import sqlite3 +from datetime import datetime +from pathlib import Path + +import click +import pandas as pd +from dotenv import load_dotenv + + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") +logger = logging.getLogger(__name__) +load_dotenv() + +# Will use this as default if no path is provided in the +# REPORT_GENERATION_DB_PATH env var +DEFAULT_DB_PATH = Path("aieng-eval-agents/aieng/agent_evals/report_generation/data/OnlineRetail.db") + + +@click.command() +@click.option("--dataset-path", required=True, help="OnlieRetail dataset CSV path.") +def main(dataset_path: str): + """Import the Online Retail dataset to the database. + + Args: + dataset_path: The path to the CSV file containing the dataset. + """ + db_path = os.getenv("REPORT_GENERATION_DB_PATH", DEFAULT_DB_PATH) + + assert Path(dataset_path).exists(), f"Dataset path {dataset_path} does not exist" + assert Path(db_path).parent.exists(), f"Database path {db_path} does not exist" + + conn = sqlite3.connect(db_path) + logger.info("Creating tables according to the OnlineRetail.ddl file") + + with open(Path("aieng-eval-agents/aieng/agent_evals/report_generation/data/OnlineRetail.ddl"), "r") as file: + conn.executescript(file.read()) + conn.commit() + + logger.info(f"Importing dataset from {dataset_path} to database at {db_path}") + + df = pd.read_csv(dataset_path) + df["InvoiceDate"] = df["InvoiceDate"].apply(convert_date) + df.to_sql("sales", conn, if_exists="append", index=False) + + conn.close() + logger.info(f"Dataset imported successfully to database at {db_path}") + + +def convert_date(date_str: str) -> str | None: + """Convert date from 'MM/DD/YY HH:MM' to 'YYYY-MM-DD HH:MM'. + + Args: + date_str: Date string in format 'MM/DD/YY HH:MM' or 'MM/DD/YY H:MM' + Example: "12/19/10 16:26" -> "2010-12-19 16:26" + + Returns + ------- + Converted date string in format 'YYYY-MM-DD HH:MM' or None if parsing fails + """ + if not date_str or date_str.strip() == "": + return None + + try: + # Parse the date - format is DD/MM/YY (day/month/year) + # Format: "12/1/10 8:26" or "12/1/10 16:26" + # Split date and time parts + parts = date_str.strip().split(" ") + if len(parts) != 2: + logger.warning(f"Invalid date format (expected 'DD/MM/YY HH:MM'): {date_str}") + return None + + date_part, time_part = parts + + # Normalize time part to have 2-digit hour + time_parts = time_part.split(":") + if len(time_parts) != 2: + logger.warning(f"Invalid time format: {time_part}") + return None + + hour, minute = time_parts + if len(hour) == 1: + hour = f"0{hour}" + time_part = f"{hour}:{minute}" + + # Parse as DD/MM/YY (day/month/year) + dt = datetime.strptime(f"{date_part} {time_part}", "%m/%d/%y %H:%M") + # Convert to YYYY-MM-DD HH:MM format + return dt.strftime("%Y-%m-%d %H:%M") + except ValueError as e: + logger.warning(f"Could not parse date: {date_str} - {e}") + return None + + +if __name__ == "__main__": + main() diff --git a/aieng-eval-agents/aieng/agent_evals/report_generation/main.py b/aieng-eval-agents/aieng/agent_evals/report_generation/main.py new file mode 100644 index 0000000..8f994b0 --- /dev/null +++ b/aieng-eval-agents/aieng/agent_evals/report_generation/main.py @@ -0,0 +1,113 @@ +"""Reason-and-Act Knowledge Retrieval Agent via the OpenAI Agent SDK.""" + +import asyncio +import logging +from typing import Any, AsyncGenerator + +import agents +import gradio as gr +from aieng.agent_evals.async_client_manager import AsyncClientManager, ReportFileWriter +from aieng.agent_evals.utils import ( + get_or_create_session, + oai_agent_stream_to_gradio_messages, +) +from dotenv import load_dotenv +from gradio.components.chatbot import ChatMessage + + +load_dotenv() + +REACT_INSTRUCTIONS = """\ +Perform the task using the SQLite database tool. \ +EACH TIME before invoking the function, you must explain your reasons for doing so. \ +If the SQL query did not return intended results, try again. \ +For best performance, divide complex queries into simpler sub-queries. \ +Do not make up information. \ +When the report is done, use the report file writer tool to write it to a file. \ +At the end, provide the report file as a downloadable hyperlink to the user. +""" + + +async def _main( + query: str, + history: list[ChatMessage], + session_state: dict[str, Any], +) -> AsyncGenerator[list[ChatMessage], Any]: + # Initialize list of chat messages for a single turn + turn_messages: list[ChatMessage] = [] + + # Construct an in-memory SQLite session for the agent to maintain + # conversation history across multiple turns of a chat + # This makes it possible to ask follow-up questions that refer to + # previous turns in the conversation + session = get_or_create_session(history, session_state) + + # Get the client manager singleton instance + client_manager = AsyncClientManager.get_instance() + + # Define an agent using the OpenAI Agent SDK + main_agent = agents.Agent( + name="Report Generation Agent", # Agent name for logging and debugging purposes + instructions=REACT_INSTRUCTIONS, # System instructions for the agent + # Tools available to the agent + # We wrap the `search_knowledgebase` method with `function_tool`, which + # will construct the tool definition JSON schema by extracting the necessary + # information from the method signature and docstring. + tools=[ + agents.function_tool(client_manager.sqlite_connection.execute), + agents.function_tool(client_manager.report_file_writer.write_report_to_file), + ], + model=agents.OpenAIChatCompletionsModel( + model=client_manager.configs.default_worker_model, + openai_client=client_manager.openai_client, + ), + ) + + # Run the agent in streaming mode to get and display intermediate outputs + result_stream = agents.Runner.run_streamed(main_agent, input=query, session=session) + + async for _item in result_stream.stream_events(): + # Parse the stream events, convert to Gradio chat messages and append to + # the chat history + turn_messages += oai_agent_stream_to_gradio_messages(_item) + if len(turn_messages) > 0: + yield turn_messages + + +if __name__ == "__main__": + load_dotenv(verbose=True) + logging.basicConfig(level=logging.INFO) + + # Disable tracing to OpenAI platform since we are using Gemini models instead + # of OpenAI models + agents.set_tracing_disabled(disabled=True) + + demo = gr.ChatInterface( + _main, + chatbot=gr.Chatbot(height=600), + textbox=gr.Textbox(lines=1, placeholder="Enter your prompt"), + # Additional input to maintain session state across multiple turns + # NOTE: Examples must be a list of lists when additional inputs are provided + additional_inputs=gr.State(value={}, render=False), + examples=[ + ["Generate a monthly sales performance report for the last year with available data."], + ["Generate a report of the top 5 selling products per year and the total sales for each product."], + ["Generate a report of the average order value per invoice per month."], + ["Generate a report with the month-over-month trends in sales."], + ["Generate a report on sales revenue by country per year."], + ["Generate a report on the 5 highest-value customers per year vs. the average customer."], + [ + "Generate a report on the average amount spent by one time buyers for each year vs. the average customer." + ], + ["Generate a report on the daily, weekly and monthly sales trends."], + ], + title="2.1: ReAct for Retrieval-Augmented Generation with OpenAI Agent SDK", + ) + + try: + demo.launch( + share=False, + allowed_paths=[ReportFileWriter.get_reports_output_path().absolute()], + ) + finally: + asyncio.run(AsyncClientManager.get_instance().close()) diff --git a/aieng-eval-agents/aieng/agent_evals/utils.py b/aieng-eval-agents/aieng/agent_evals/utils.py new file mode 100644 index 0000000..7cecc7d --- /dev/null +++ b/aieng-eval-agents/aieng/agent_evals/utils.py @@ -0,0 +1,96 @@ +"""Utility functions for the report generation agent.""" + +import uuid +from typing import Any + +from agents import SQLiteSession, StreamEvent, stream_events +from agents.items import ToolCallOutputItem +from gradio.components.chatbot import ChatMessage, MetadataDict +from openai.types.responses import ResponseFunctionToolCall, ResponseOutputText +from openai.types.responses.response_completed_event import ResponseCompletedEvent +from openai.types.responses.response_output_message import ResponseOutputMessage + + +def oai_agent_stream_to_gradio_messages(stream_event: StreamEvent) -> list[ChatMessage]: + """Parse agent sdk "stream event" into a list of gr messages. + + Adds extra data for tool use to make the gradio display informative. + """ + output: list[ChatMessage] = [] + + if isinstance(stream_event, stream_events.RawResponsesStreamEvent): + data = stream_event.data + if isinstance(data, ResponseCompletedEvent): + # The completed event may contain multiple output messages, + # including tool calls and final outputs. + # If there is at least one tool call, we mark the response as a thought. + is_thought = len(data.response.output) > 1 and any( + isinstance(message, ResponseFunctionToolCall) for message in data.response.output + ) + + for message in data.response.output: + if isinstance(message, ResponseOutputMessage): + for _item in message.content: + if isinstance(_item, ResponseOutputText): + output.append( + ChatMessage( + role="assistant", + content=_item.text, + metadata={ + "title": "🧠 Thought", + "id": data.sequence_number, + } + if is_thought + else MetadataDict(), + ) + ) + elif isinstance(message, ResponseFunctionToolCall): + output.append( + ChatMessage( + role="assistant", + content=f"```\n{message.arguments}\n```", + metadata={ + "title": f"🛠️ Used tool `{message.name}`", + }, + ) + ) + + elif isinstance(stream_event, stream_events.RunItemStreamEvent): + name = stream_event.name + item = stream_event.item + + if name == "tool_output" and isinstance(item, ToolCallOutputItem): + output.append( + ChatMessage( + role="assistant", + content=f"```\n{item.output}\n```", + metadata={ + "title": "*Tool call output*", + "status": "done", # This makes it collapsed by default + }, + ) + ) + + return output + + +def get_or_create_session( + history: list[ChatMessage], + session_state: dict[str, Any], +) -> SQLiteSession: + """Get existing session or create a new one for conversation persistence. + + Args: + history: The history of the conversation. + session_state: The state of the session. + + Returns + ------- + The session. + """ + if len(history) == 0: + session = SQLiteSession(session_id=str(uuid.uuid4())) + session_state["session"] = session + else: + session = session_state["session"] + return session diff --git a/pyproject.toml b/pyproject.toml index 5129452..ef71b33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "scikit-learn>=1.7.0", "weaviate-client>=4.18.3", "urllib3>=2.6.3", + "openpyxl>=3.1.5", "authlib>=1.6.6", "filelock>=3.20.3", "pyasn1>=0.6.2", @@ -81,7 +82,7 @@ aieng-eval-agents = { workspace = true } [tool.ruff] include = ["*.py", "pyproject.toml", "*.ipynb"] -line-length = 88 +line-length = 120 [tool.ruff.format] quote-style = "double" diff --git a/uv.lock b/uv.lock index a2a8989..861b055 100644 --- a/uv.lock +++ b/uv.lock @@ -32,6 +32,7 @@ dependencies = [ { name = "numpy" }, { name = "openai" }, { name = "openai-agents" }, + { name = "openpyxl" }, { name = "plotly" }, { name = "pyasn1" }, { name = "pydantic" }, @@ -94,6 +95,7 @@ requires-dist = [ { name = "numpy", specifier = "<2.3.0" }, { name = "openai", specifier = ">=2.8.1" }, { name = "openai-agents", specifier = ">=0.6.1" }, + { name = "openpyxl", specifier = ">=3.1.5" }, { name = "plotly", specifier = ">=6.5.0" }, { name = "pyasn1", specifier = ">=0.6.2" }, { name = "pydantic", specifier = ">=2.12.4" }, @@ -1068,6 +1070,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + [[package]] name = "executing" version = "2.2.0" @@ -3180,6 +3191,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/53/d8076306f324992c79e9b2ee597f2ce863f0ac5d1fd24e6ad88f2a4dcbc0/openai_agents-0.6.1-py3-none-any.whl", hash = "sha256:7bde01c8d2fd723b0c72c9b207dcfeb12a8d211078f5d259945fb163a6f52b89", size = 237609, upload-time = "2025-11-20T01:17:06.454Z" }, ] +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.38.0"