diff --git a/agentops/session.py b/agentops/session.py index 3cfc1303..1213e2fd 100644 --- a/agentops/session.py +++ b/agentops/session.py @@ -5,7 +5,7 @@ import time from decimal import ROUND_HALF_UP, Decimal from termcolor import colored -from typing import Optional, List, Union +from typing import Any, Optional, List, Union from uuid import UUID, uuid4 from datetime import datetime @@ -15,7 +15,7 @@ from .log_config import logger from .config import Configuration from .helpers import get_ISO_time, filter_unjsonable, safe_serialize -from .http_client import HttpClient +from .http_client import HttpClient, Response class Session: @@ -24,14 +24,30 @@ class Session: Args: session_id (UUID): The session id is used to record particular runs. + config (Configuration): The configuration object for the session. tags (List[str], optional): Tags that can be used for grouping or sorting later. Examples could be ["GPT-4"]. + host_env (dict, optional): A dictionary containing host and environment data. Attributes: - init_timestamp (float): The timestamp for when the session started, represented as seconds since the epoch. - end_timestamp (float, optional): The timestamp for when the session ended, represented as seconds since the epoch. This is only set after end_session is called. - end_state (str, optional): The final state of the session. Suggested: "Success", "Fail", "Indeterminate". Defaults to "Indeterminate". + init_timestamp (str): The ISO timestamp for when the session started. + end_timestamp (str, optional): The ISO timestamp for when the session ended. Only set after end_session is called. + end_state (str, optional): The final state of the session. Options: "Success", "Fail", "Indeterminate". Defaults to "Indeterminate". end_state_reason (str, optional): The reason for ending the session. - + session_id (UUID): Unique identifier for the session. + tags (List[str]): List of tags associated with the session for grouping and filtering. + video (str, optional): URL to a video recording of the session. + host_env (dict, optional): Dictionary containing host and environment data. + config (Configuration): Configuration object containing settings for the session. + jwt (str, optional): JSON Web Token for authentication with the AgentOps API. + token_cost (Decimal): Running total of token costs for the session. + event_counts (dict): Counter for different types of events: + - llms: Number of LLM calls + - tools: Number of tool calls + - actions: Number of actions + - errors: Number of errors + - apis: Number of API calls + session_url (str, optional): URL to view the session in the AgentOps dashboard. + is_running (bool): Flag indicating if the session is currently active. """ def __init__( @@ -52,7 +68,8 @@ def __init__( self.config = config self.jwt = None self.lock = threading.Lock() - self.queue = [] + self.queue: List[Any] = [] + self.token_cost = Decimal(0) self.event_counts = { "llms": 0, "tools": 0, @@ -60,6 +77,7 @@ def __init__( "errors": 0, "apis": 0, } + self.session_url: Optional[str] = None self.stop_flag = threading.Event() self.thread = threading.Thread(target=self._run) @@ -87,10 +105,11 @@ def end_session( video: Optional[str] = None, ) -> Union[Decimal, None]: if not self.is_running: - return + return None if not any(end_state == state.value for state in EndState): - return logger.warning("Invalid end_state. Please use one of the EndState enums") + logger.warning("Invalid end_state. Please use one of the EndState enums") + return None self.end_timestamp = get_ISO_time() self.end_state = end_state @@ -101,77 +120,28 @@ def end_session( self.stop_flag.set() self.thread.join(timeout=1) self._flush_queue() - - def format_duration(start_time, end_time): - start = datetime.fromisoformat(start_time.replace("Z", "+00:00")) - end = datetime.fromisoformat(end_time.replace("Z", "+00:00")) - duration = end - start - - hours, remainder = divmod(duration.total_seconds(), 3600) - minutes, seconds = divmod(remainder, 60) - - parts = [] - if hours > 0: - parts.append(f"{int(hours)}h") - if minutes > 0: - parts.append(f"{int(minutes)}m") - parts.append(f"{seconds:.1f}s") - - return " ".join(parts) - - with self.lock: - payload = {"session": self.__dict__} - try: - res = HttpClient.post( - f"{self.config.endpoint}/v2/update_session", - json.dumps(filter_unjsonable(payload)).encode("utf-8"), - jwt=self.jwt, - ) - except ApiServerException as e: - return logger.error(f"Could not end session - {e}") - - logger.debug(res.body) - token_cost = res.body.get("token_cost", "unknown") - - formatted_duration = format_duration(self.init_timestamp, self.end_timestamp) - - if token_cost == "unknown" or token_cost is None: - token_cost_d = Decimal(0) - else: - token_cost_d = Decimal(token_cost) - - formatted_cost = ( - "{:.2f}".format(token_cost_d) - if token_cost_d == 0 - else "{:.6f}".format(token_cost_d.quantize(Decimal("0.000001"), rounding=ROUND_HALF_UP)) - ) + analytics_stats = self.get_analytics() analytics = ( f"Session Stats - " - f"{colored('Duration:', attrs=['bold'])} {formatted_duration} | " - f"{colored('Cost:', attrs=['bold'])} ${formatted_cost} | " - f"{colored('LLMs:', attrs=['bold'])} {self.event_counts['llms']} | " - f"{colored('Tools:', attrs=['bold'])} {self.event_counts['tools']} | " - f"{colored('Actions:', attrs=['bold'])} {self.event_counts['actions']} | " - f"{colored('Errors:', attrs=['bold'])} {self.event_counts['errors']}" + f"{colored('Duration:', attrs=['bold'])} {analytics_stats['Duration']} | " + f"{colored('Cost:', attrs=['bold'])} ${analytics_stats['Cost']} | " + f"{colored('LLMs:', attrs=['bold'])} {analytics_stats['LLM calls']} | " + f"{colored('Tools:', attrs=['bold'])} {analytics_stats['Tool calls']} | " + f"{colored('Actions:', attrs=['bold'])} {analytics_stats['Actions']} | " + f"{colored('Errors:', attrs=['bold'])} {analytics_stats['Errors']}" ) logger.info(analytics) - session_url = res.body.get( - "session_url", - f"https://app.agentops.ai/drilldown?session_id={self.session_id}", - ) - logger.info( colored( - f"\x1b[34mSession Replay: {session_url}\x1b[0m", + f"\x1b[34mSession Replay: {self.session_url}\x1b[0m", "blue", ) ) - active_sessions.remove(self) - return token_cost_d + return self.token_cost def add_tags(self, tags: List[str]) -> None: """ @@ -388,5 +358,80 @@ def wrapper(*args, **kwargs): return wrapper + @staticmethod + def _format_duration(start_time, end_time): + start = datetime.fromisoformat(start_time.replace("Z", "+00:00")) + end = datetime.fromisoformat(end_time.replace("Z", "+00:00")) + duration = end - start + + hours, remainder = divmod(duration.total_seconds(), 3600) + minutes, seconds = divmod(remainder, 60) + + parts = [] + if hours > 0: + parts.append(f"{int(hours)}h") + if minutes > 0: + parts.append(f"{int(minutes)}m") + parts.append(f"{seconds:.1f}s") + + return " ".join(parts) + + def _get_response(self) -> Optional[Response]: + with self.lock: + payload = {"session": self.__dict__} + try: + response = HttpClient.post( + f"{self.config.endpoint}/v2/update_session", + json.dumps(filter_unjsonable(payload)).encode("utf-8"), + jwt=self.jwt, + ) + except ApiServerException as e: + logger.error(f"Could not fetch response from server - {e}") + return None + + logger.debug(response.body) + return response + + def _get_token_cost(self, response: Response) -> Decimal: + token_cost = response.body.get("token_cost", "unknown") + if token_cost == "unknown" or token_cost is None: + return Decimal(0) + return Decimal(token_cost) + + @staticmethod + def _format_token_cost(token_cost_d): + return ( + "{:.2f}".format(token_cost_d) + if token_cost_d == 0 + else "{:.6f}".format(token_cost_d.quantize(Decimal("0.000001"), rounding=ROUND_HALF_UP)) + ) + + def get_analytics(self) -> Optional[dict[str, Union[Decimal, str]]]: + if not self.end_timestamp: + self.end_timestamp = get_ISO_time() + + formatted_duration = self._format_duration(self.init_timestamp, self.end_timestamp) + + response = self._get_response() + if response is None: + return None + + self.token_cost = self._get_token_cost(response) + formatted_cost = self._format_token_cost(self.token_cost) + + self.session_url = response.body.get( + "session_url", + f"https://app.agentops.ai/drilldown?session_id={self.session_id}", + ) + + return { + "LLM calls": self.event_counts["llms"], + "Tool calls": self.event_counts["tools"], + "Actions": self.event_counts["actions"], + "Errors": self.event_counts["errors"], + "Duration": formatted_duration, + "Cost": formatted_cost, + } + active_sessions: List[Session] = [] diff --git a/docs/images/external/ollama/ollama-icon.png b/docs/images/external/ollama/ollama-icon.png new file mode 100644 index 00000000..46060de8 Binary files /dev/null and b/docs/images/external/ollama/ollama-icon.png differ diff --git a/docs/mint.json b/docs/mint.json index d2043620..45e61b45 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -92,6 +92,7 @@ "v1/integrations/langchain", "v1/integrations/cohere", "v1/integrations/anthropic", + "v1/integrations/ollama", "v1/integrations/litellm", "v1/integrations/multion", "v1/integrations/rest" diff --git a/docs/v1/concepts/sessions.mdx b/docs/v1/concepts/sessions.mdx index 1da20fb3..68beaf02 100644 --- a/docs/v1/concepts/sessions.mdx +++ b/docs/v1/concepts/sessions.mdx @@ -51,6 +51,9 @@ Optionally, sessions may include: _Note: Overrides any current tags_ +#### `get_analytics` +**Returns** (dict): A dictionary containing various analytics metrics for the session. + ## Starting a Session When you call `agentops.init()`, a session is automatically started. @@ -62,7 +65,7 @@ Both `agentops.init()` and `agentops.start_session()` work as a factory pattern ## Ending a Session If a process ends without any call to agentops, it will show in the dashboard as `Indeterminate`. -To end with a state, call either `agentops.end_session(...)` [(reference)](/v1/usage/sdk-reference/#end-session) if only one session is in use. Otherwise use `session.end_session(...)` +To end with a state, call either `agentops.end_session(...)` [(reference)](/v1/usage/sdk-reference/#end-session) if only one session is in use. Otherwise use `session.end_session(...)`. ## Inherited Sessions When working with multiple agents running in different processes, it's possible to initialize AgentOps or start a session @@ -71,22 +74,51 @@ with an existing session_id. `agentops.init(inherited_session_id=)` `agentops.start_session(inherited_session_id=)` -You can retrieve the current `session_id` by assigning the returned value from `init()` or `start_session()` +You can retrieve the current `session_id` by assigning the returned value from `init()` or `start_session()`. -```python python + +```python import agentops session = agentops.init() # pass session.session_id to the other process +``` +```python # -- other process -- session_id = retrieve_session_id() # <-- your function agentops.init(inherited_session_id=) ``` + Both processes will now contribute data to the same session. +## Session Analytics +You can retrieve the analytics for a session by calling `session.get_analytics()`. + +The example below shows how to record events and retrieve analytics. + + + +```python +import agentops +session = agentops.init() +session.record(ActionEvent("llms")) +session.record(ActionEvent("tools")) +analytics = session.get_analytics() +print(analytics) +session.end_session("Success") +``` + +The output will look like this - + +```bash +{'LLM calls': 0, 'Tool calls': 0, 'Actions': 0, 'Errors': 0, 'Duration': '0.9s', 'Cost': '0.00'} +``` + + + ## The AgentOps SDK Client _More info for the curious_ diff --git a/docs/v1/examples/examples.mdx b/docs/v1/examples/examples.mdx index 57765789..85b4583d 100644 --- a/docs/v1/examples/examples.mdx +++ b/docs/v1/examples/examples.mdx @@ -31,6 +31,9 @@ mode: "wide" Create an autonomous browser agent capable of navigating the web and extracting information + } iconType="image" href="/v1/examples/ollama"> + Simple Ollama integration with AgentOps + ## Video Guides diff --git a/docs/v1/examples/ollama.mdx b/docs/v1/examples/ollama.mdx new file mode 100644 index 00000000..a297f192 --- /dev/null +++ b/docs/v1/examples/ollama.mdx @@ -0,0 +1,123 @@ +--- +title: 'Ollama Example' +description: 'Using Ollama with AgentOps' +mode: "wide" +--- + +{/* SOURCE_FILE: examples/ollama_examples/ollama_examples.ipynb */}# AgentOps Ollama Integration + +This example demonstrates how to use AgentOps to monitor your Ollama LLM calls. + +First let's install the required packages + +> ⚠️ **Important**: Make sure you have Ollama installed and running locally before running this notebook. You can install it from [ollama.ai](https://ollama.com). + + +```python +%pip install -U ollama +%pip install -U agentops +%pip install -U python-dotenv +``` + +Then import them + + +```python +import ollama +import agentops +import os +from dotenv import load_dotenv + +``` + +Next, we'll set our API keys. For Ollama, we'll need to make sure Ollama is running locally. +[Get an AgentOps API key](https://agentops.ai/settings/projects) + +1. Create an environment variable in a .env file or other method. By default, the AgentOps `init()` function will look for an environment variable named `AGENTOPS_API_KEY`. Or... +2. Replace `` below and pass in the optional `api_key` parameter to the AgentOps `init(api_key=...)` function. Remember not to commit your API key to a public repo! + + +```python +# Let's load our environment variables +load_dotenv() + +AGENTOPS_API_KEY = os.getenv("AGENTOPS_API_KEY") or "" +``` + + +```python +# Initialize AgentOps with some default tags +agentops.init(AGENTOPS_API_KEY, default_tags=["ollama-example"]) +``` + +Now let's make some basic calls to Ollama. Make sure you have pulled the model first, use the following or replace with whichever model you want to use. + + +```python +ollama.pull("mistral") +``` + + +```python +# Basic completion, +response = ollama.chat(model='mistral', + messages=[{ + 'role': 'user', + 'content': 'What are the benefits of using AgentOps for monitoring LLMs?', + }] +) +print(response['message']['content']) +``` + +Let's try streaming responses as well + + +```python +# Streaming Example +stream = ollama.chat( + model='mistral', + messages=[{ + 'role': 'user', + 'content': 'Write a haiku about monitoring AI agents', + }], + stream=True +) + +for chunk in stream: + print(chunk['message']['content'], end='') + +``` + + +```python +# Conversation Example +messages = [ + { + 'role': 'user', + 'content': 'What is AgentOps?' + }, + { + 'role': 'assistant', + 'content': 'AgentOps is a monitoring and observability platform for LLM applications.' + }, + { + 'role': 'user', + 'content': 'Can you give me 3 key features?' + } +] + +response = ollama.chat( + model='mistral', + messages=messages +) +print(response['message']['content']) +``` + +> 💡 **Note**: In production environments, you should add proper error handling around the Ollama calls and use `agentops.end_session("Error")` when exceptions occur. + +Finally, let's end our AgentOps session + + +```python +agentops.end_session("Success") +``` diff --git a/docs/v1/integrations/ollama.mdx b/docs/v1/integrations/ollama.mdx new file mode 100644 index 00000000..31e6512d --- /dev/null +++ b/docs/v1/integrations/ollama.mdx @@ -0,0 +1,150 @@ +--- +title: Ollama +description: "AgentOps provides first class support for Ollama" +--- + +import CodeTooltip from '/snippets/add-code-tooltip.mdx' +import EnvTooltip from '/snippets/add-env-tooltip.mdx' + + +This is a living integration. Should you need any added functionality, message us on [Discord](https://discord.gg/UgJyyxx7uc)! + + +} iconType="image" href="https://ollama.com"> + First class support for Ollama + + + + + + ```bash pip + pip install agentops ollama + ``` + ```bash poetry + poetry add agentops ollama + ``` + + + + + + ```python python + import agentops + import ollama + + agentops.init() + agentops.start_session() + + ollama.pull("") + + response = ollama.chat(model='mistral', + messages=[{ + 'role': 'user', + 'content': 'What are the benefits of using AgentOps for monitoring LLMs?', + }] + ) + print(response['message']['content']) + ... + # End of program (e.g. main.py) + agentops.end_session("Success") # Success|Fail|Indeterminate + ``` + + + + ```python .env + # Alternatively, you can set the API key as an environment variable + AGENTOPS_API_KEY= + ``` + + Read more about environment variables in [Advanced Configuration](/v1/usage/advanced-configuration) + + + Execute your program and visit [app.agentops.ai/drilldown](https://app.agentops.ai/drilldown) to observe your Agent! 🕵️ + + After your run, AgentOps prints a clickable url to console linking directly to your session in the Dashboard + +
+ + + +## Full Examples + + + ```python basic completion + import ollama + import agentops + + agentops.init() + + ollama.pull("") + response = ollama.chat( + model="", + max_tokens=1024, + messages=[{ + "role": "user", + "content": "Write a haiku about AI and humans working together" + }] + ) + + print(response['message']['content']) + agentops.end_session('Success') + ``` + + ```python streaming + import agentops + import ollama + + async def main(): + agentops.init() + ollama.pull("") + + stream = ollama.chat( + model="", + messages=[{ + 'role': 'user', + 'content': 'Write a haiku about monitoring AI agents', + }], + stream=True + ) + + for chunk in stream: + print(chunk['message']['content'], end='') + + agentops.end_session('Success') + ``` + + ```python conversation + import ollama + import agentops + + agentops.init() + ollama.pull("") + + messages = [ + { + 'role': 'user', + 'content': 'What is AgentOps?' + }, + { + 'role': 'assistant', + 'content': 'AgentOps is a monitoring and observability platform for LLM applications.' + }, + { + 'role': 'user', + 'content': 'Can you give me 3 key features?' + } +] + + response = ollama.chat( + model="", + messages=messages + ) + print(response['message']['content']) + agentops.end_session('Success') + ``` + + + + + + diff --git a/examples/ollama_examples/ollama_examples.ipynb b/examples/ollama_examples/ollama_examples.ipynb new file mode 100644 index 00000000..c876ef7a --- /dev/null +++ b/examples/ollama_examples/ollama_examples.ipynb @@ -0,0 +1,212 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# AgentOps Ollama Integration\n", + "\n", + "This example demonstrates how to use AgentOps to monitor your Ollama LLM calls.\n", + "\n", + "First let's install the required packages\n", + "\n", + "> ⚠️ **Important**: Make sure you have Ollama installed and running locally before running this notebook. You can install it from [ollama.ai](https://ollama.com)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install -U ollama\n", + "%pip install -U agentops\n", + "%pip install -U python-dotenv" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then import them" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import ollama\n", + "import agentops\n", + "import os\n", + "from dotenv import load_dotenv\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we'll set our API keys. For Ollama, we'll need to make sure Ollama is running locally.\n", + "[Get an AgentOps API key](https://agentops.ai/settings/projects)\n", + "\n", + "1. Create an environment variable in a .env file or other method. By default, the AgentOps `init()` function will look for an environment variable named `AGENTOPS_API_KEY`. Or...\n", + "2. Replace `` below and pass in the optional `api_key` parameter to the AgentOps `init(api_key=...)` function. Remember not to commit your API key to a public repo!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Let's load our environment variables\n", + "load_dotenv()\n", + "\n", + "AGENTOPS_API_KEY = os.getenv(\"AGENTOPS_API_KEY\") or \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize AgentOps with some default tags\n", + "agentops.init(AGENTOPS_API_KEY, default_tags=[\"ollama-example\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's make some basic calls to Ollama. Make sure you have pulled the model first, use the following or replace with whichever model you want to use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ollama.pull(\"mistral\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Basic completion,\n", + "response = ollama.chat(model='mistral',\n", + " messages=[{\n", + " 'role': 'user',\n", + " 'content': 'What are the benefits of using AgentOps for monitoring LLMs?',\n", + " }]\n", + ")\n", + "print(response['message']['content'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's try streaming responses as well" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Streaming Example\n", + "stream = ollama.chat(\n", + " model='mistral',\n", + " messages=[{\n", + " 'role': 'user',\n", + " 'content': 'Write a haiku about monitoring AI agents',\n", + " }],\n", + " stream=True\n", + ")\n", + "\n", + "for chunk in stream:\n", + " print(chunk['message']['content'], end='')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Conversation Example\n", + "messages = [\n", + " {\n", + " 'role': 'user',\n", + " 'content': 'What is AgentOps?'\n", + " },\n", + " {\n", + " 'role': 'assistant',\n", + " 'content': 'AgentOps is a monitoring and observability platform for LLM applications.'\n", + " },\n", + " {\n", + " 'role': 'user',\n", + " 'content': 'Can you give me 3 key features?'\n", + " }\n", + "]\n", + "\n", + "response = ollama.chat(\n", + " model='mistral',\n", + " messages=messages\n", + ")\n", + "print(response['message']['content'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> 💡 **Note**: In production environments, you should add proper error handling around the Ollama calls and use `agentops.end_session(\"Error\")` when exceptions occur." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, let's end our AgentOps session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agentops.end_session(\"Success\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "gpt_desk", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/test_session.py b/tests/test_session.py index 1392cc91..25ad8246 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -183,6 +183,43 @@ def test_safe_get_session_with_multiple_sessions(self, mock_req): session = Client()._safe_get_session() assert session is None + def test_get_analytics(self, mock_req): + # Arrange + session = agentops.start_session() + session.add_tags(["test-session-analytics-tag"]) + assert session is not None + + # Record some events to increment counters + session.record(ActionEvent("llms")) + session.record(ActionEvent("tools")) + session.record(ActionEvent("actions")) + session.record(ActionEvent("errors")) + time.sleep(0.1) + + # Act + analytics = session.get_analytics() + + # Assert + assert isinstance(analytics, dict) + assert all(key in analytics for key in ["LLM calls", "Tool calls", "Actions", "Errors", "Duration", "Cost"]) + + # Check specific values + assert analytics["LLM calls"] == 1 + assert analytics["Tool calls"] == 1 + assert analytics["Actions"] == 1 + assert analytics["Errors"] == 1 + + # Check duration format + assert isinstance(analytics["Duration"], str) + assert "s" in analytics["Duration"] + + # Check cost format (mock returns token_cost: 5) + assert analytics["Cost"] == "5.000000" + + # End session and cleanup + session.end_session(end_state="Success") + agentops.end_all_sessions() + class TestMultiSessions: def setup_method(self): @@ -276,3 +313,49 @@ def test_add_tags(self, mock_req): "session-2", "session-2-added", ] + + def test_get_analytics_multiple_sessions(self, mock_req): + session_1 = agentops.start_session() + session_1.add_tags(["session-1", "test-analytics-tag"]) + session_2 = agentops.start_session() + session_2.add_tags(["session-2", "test-analytics-tag"]) + assert session_1 is not None + assert session_2 is not None + + # Record events in the sessions + session_1.record(ActionEvent("llms")) + session_1.record(ActionEvent("tools")) + session_2.record(ActionEvent("actions")) + session_2.record(ActionEvent("errors")) + + time.sleep(1.5) + + # Act + analytics_1 = session_1.get_analytics() + analytics_2 = session_2.get_analytics() + + # Assert 2 record_event requests - 2 for each session + assert analytics_1["LLM calls"] == 1 + assert analytics_1["Tool calls"] == 1 + assert analytics_1["Actions"] == 0 + assert analytics_1["Errors"] == 0 + + assert analytics_2["LLM calls"] == 0 + assert analytics_2["Tool calls"] == 0 + assert analytics_2["Actions"] == 1 + assert analytics_2["Errors"] == 1 + + # Check duration format + assert isinstance(analytics_1["Duration"], str) + assert "s" in analytics_1["Duration"] + assert isinstance(analytics_2["Duration"], str) + assert "s" in analytics_2["Duration"] + + # Check cost format (mock returns token_cost: 5) + assert analytics_1["Cost"] == "5.000000" + assert analytics_2["Cost"] == "5.000000" + + end_state = "Success" + + session_1.end_session(end_state) + session_2.end_session(end_state)