diff --git a/.gitignore b/.gitignore index f799b7221..807d19890 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,9 @@ celerybeat.pid .env.* .venv +# exception for local langfuse init vars +!**/packages/exchange/.env.langfuse.local + # Spyder project settings .spyderproject .spyproject diff --git a/.goosehints b/.goosehints index 8b6535a63..bb6acad17 100644 --- a/.goosehints +++ b/.goosehints @@ -1,3 +1,13 @@ This is a python CLI app that uses UV. Read CONTRIBUTING.md for information on how to build and test it as needed. -Some key concepts are that it is run as a command line interface, dependes on the "ai-exchange" package, and has the concept of toolkits which are ways that its behavior can be extended. Look in src/goose and tests. -Once the user has UV installed it should be able to be used effectively along with uvx to run tasks as needed + +Some key concepts are that it is run as a command line interface, dependes on the "ai-exchange" package (which is in packages/exchange in this repo), and has the concept of toolkits which are ways that its behavior can be extended. Look in src/goose and tests. + +Assume the user has UV installed and ensure UV is used to run any python related commands. + +To run tests: + +```sh +uv sync && uv run pytest tests -m 'not integration' +``` + +ideally after each change \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 18cef1199..76feea9e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.5] - 2024-10-15 +- chore: updates ollama default model from mistral-nemo to qwen2.5 (#150) +- feat: add vision support for Google (#141) +- fix: session resume with arg handled incorrectly (#145) +- docs: add release instructions to CONTRIBUTING.md (#143) +- docs: add link to action, IDE words (#140) +- docs: goosehints doc fix only (#142) + +## [0.9.4] - 2024-10-10 + +- revert: "feat: add local langfuse tracing option (#106)" +- feat: add local langfuse tracing option (#106) +- feat: add groq provider (#134) +- feat: add a deep thinking reasoner model (o1-preview/mini) (#68) +- fix: use concrete SessionNotifier (#135) +- feat: add guards to session management (#101) +- fix: Set default model configuration for the Google provider. (#131) +- test: convert Google Gemini tests to VCR (#118) +- chore: Add goose providers list command (#116) +- docs: working ollama for desktop (#125) +- docs: format and clean up warnings/errors (#120) +- docs: update deploy workflow (#124) +- feat: Implement a goose run command (#121) +- feat: saved api_key to keychain for user (#104) +- docs: add callout plugin (#119) +- chore: add a page to docs for Goose application examples (#117) +- fix: exit the goose and show the error message when provider environment variable is not set (#103) +- fix: Update OpenAI pricing per https://openai.com/api/pricing/ (#110) +- fix: update developer tool prompts to use plan task status to match allowable statuses update_plan tool call (#107) +- fix: removed the panel in the output so that the user won't have unnecessary pane borders in the copied content (#109) +- docs: update links to exchange to the new location (#108) +- chore: setup workspace for exchange (#105) +- fix: resolve uvx when using a git client or IDE (#98) +- ci: add include-markdown for mkdocs (#100) +- chore: fix broken badge on readme (#102) +- feat: add global optional user goosehints file (#73) +- docs: update docs (#99) + ## [0.9.3] - 2024-09-25 - feat: auto save sessions before next user input (#94) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 783a010a6..deea38211 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,6 +48,21 @@ or, as a shortcut, just test ``` +### Enable traces in Goose with [locally hosted Langfuse](https://langfuse.com/docs/deployment/self-host) +> [!NOTE] +> This integration is experimental and we don't currently have integration tests for it. + +Developers can use locally hosted Langfuse tracing by applying the custom `observe_wrapper` decorator defined in `packages/exchange/src/langfuse_wrapper.py` to functions for automatic integration with Langfuse. + +- Run `just langfuse-server` to start your local Langfuse server. It requires Docker. +- Go to http://localhost:3000 and log in with the default email/password output by the shell script (values can also be found in the `.env.langfuse.local` file). +- Run Goose with the --tracing flag enabled i.e., `goose session start --tracing` +- View your traces at http://localhost:3000 + +To extend tracing to additional functions, import `from exchange.langfuse_wrapper import observe_wrapper` and use the `observe_wrapper()` decorator on functions you wish to enable tracing for. `observe_wrapper` functions the same way as Langfuse's observe decorator. + +Read more about Langfuse's decorator-based tracing [here](https://langfuse.com/docs/sdk/python/decorators). + ## Exchange The lower level generation behind goose is powered by the [`exchange`][ai-exchange] package, also in this repo. @@ -73,6 +88,16 @@ Additions to the [developer toolkit][developer] change the core performance, and This project follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for PR titles. Conventional Commits make it easier to understand the history of a project and facilitate automation around versioning and changelog generation. +## Release + +In order to release a new version of goose, you need to do the following: +1. Update CHANGELOG.md. To get the commit messages since last release, run: `just release-notes` +2. Update version in `pyproject.toml` for `goose` and package dependencies such as `exchange` +3. Create a PR and merge it into main branch +4. Tag the HEAD commit in main branch. To do this, switch to main branch and run: `just tag-push` +5. Publish a new release from the [Github Release UI](https://github.com/block-open-source/goose/releases) + + [issues]: https://github.com/block-open-source/goose/issues [goose-plugins]: https://github.com/block-open-source/goose-plugins [ai-exchange]: https://github.com/block-open-source/goose/tree/main/packages/exchange diff --git a/README.md b/README.md index 6f980ff76..0cbaf35e2 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,6 @@ To install Goose, use `pipx`. First ensure [pipx][pipx] is installed: brew install pipx pipx ensurepath ``` -You can also place `.goosehints` in `~/.config/goose/.goosehints` if you like for always loaded hints personal to you. Then install Goose: @@ -131,7 +130,21 @@ You will see the Goose prompt `G❯`: G❯ type your instructions here exactly as you would tell a developer. ``` -Now you are interacting with Goose in conversational sessions - something like a natural language driven code interpreter. The default toolkit allows Goose to take actions through shell commands and file edits. You can interrupt Goose with `CTRL+D` or `ESC+Enter` at any time to help redirect its efforts. +Now you are interacting with Goose in conversational sessions - think of it as like giving direction to a junior developer. The default toolkit allows Goose to take actions through shell commands and file edits. You can interrupt Goose with `CTRL+D` or `ESC+Enter` at any time to help redirect its efforts. + +> [!TIP] +> You can place a `.goosehints` text file in any directory you launch goose from to give it some background info for new sessions in plain language (eg how to test, what instructions to read to get started or just tell it to read the README!) You can also put a global one `~/.config/goose/.goosehints` if you like for always loaded hints personal to you. + +### Running a goose tasks (one off) + +You can run goose to do things just as a one off, such as tidying up, and then exiting: + +```sh +goose run instructions.md +``` + +This will run until completion as best it can. You can also pass `--resume-session` and it will re-use the first session it finds for context + #### Exit the session @@ -147,16 +160,55 @@ goose session resume To see more documentation on the CLI commands currently available to Goose check out the documentation [here][cli]. If you’d like to develop your own CLI commands for Goose, check out the [Contributing document][contributing]. +### Tracing with Langfuse +> [!NOTE] +> This Langfuse integration is experimental and we don't currently have integration tests for it. + +The exchange package provides a [Langfuse](https://langfuse.com/) wrapper module. The wrapper serves to initialize Langfuse appropriately if the Langfuse server is running locally and otherwise to skip applying the Langfuse observe descorators. + +#### Start your local Langfuse server + +Run `just langfuse-server` to start your local Langfuse server. It requires Docker. + +Read more about local Langfuse deployments [here](https://langfuse.com/docs/deployment/local). + +#### Exchange and Goose integration + +Import `from exchange.langfuse_wrapper import observe_wrapper` and use the `observe_wrapper()` decorator on functions you wish to enable tracing for. `observe_wrapper` functions the same way as Langfuse's observe decorator. + +Read more about Langfuse's decorator-based tracing [here](https://langfuse.com/docs/sdk/python/decorators). + +In Goose, initialization requires certain environment variables to be present: + +- `LANGFUSE_PUBLIC_KEY`: Your Langfuse public key +- `LANGFUSE_SECRET_KEY`: Your Langfuse secret key +- `LANGFUSE_BASE_URL`: The base URL of your Langfuse instance + +By default your local deployment and Goose will use the values in `.env.langfuse.local`. + + + ### Next steps Learn how to modify your Goose profiles.yaml file to add and remove functionality (toolkits) and providing context to get the most out of Goose in our [Getting Started Guide][getting-started]. +## Other ways to run goose + **Want to move out of the terminal and into an IDE?** We have some experimental IDE integrations for VSCode and JetBrains IDEs: * https://github.com/square/goose-vscode * https://github.com/Kvadratni/goose-intellij +**Goose as a Github Action** + +There is also an experimental Github action to run goose as part of your workflow (for example if you ask it to fix an issue): +https://github.com/marketplace/actions/goose-ai-developer-agent + +**With Docker** + +There is also a `Dockerfile` in the root of this project you can use if you want to run goose in a sandboxed fashion. + ## Getting involved! There is a lot to do! If you're interested in contributing, a great place to start is picking a `good-first-issue`-labelled ticket from our [issues list][gh-issues]. More details on how to develop Goose can be found in our [Contributing Guide][contributing]. We are a friendly, collaborative group and look forward to working together![^1] diff --git a/docs/plugins/cli.md b/docs/plugins/cli.md index 5d27563c9..7a3566d34 100644 --- a/docs/plugins/cli.md +++ b/docs/plugins/cli.md @@ -19,11 +19,13 @@ Lists the version of Goose and any associated plugins. **Usage:** ```sh - goose session start [--profile PROFILE] [--plan PLAN] + goose session start [--profile PROFILE] [--plan PLAN] [--log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]] [--tracing] ``` Starts a new Goose session. +If you want to enable locally hosted Langfuse tracing, pass the --tracing flag after starting your local Langfuse server as outlined in the [Contributing Guide's][contributing] Development guidelines. + #### `resume` **Usage:** diff --git a/docs/plugins/providers.md b/docs/plugins/providers.md index 7527f798a..63e5e80f1 100644 --- a/docs/plugins/providers.md +++ b/docs/plugins/providers.md @@ -8,6 +8,7 @@ Providers in Goose mean "LLM providers" that Goose can interact with. Providers * Azure * Bedrock * Databricks +* Google * Ollama * OpenAI diff --git a/justfile b/justfile index 8b2ca2338..96f51d6b8 100644 --- a/justfile +++ b/justfile @@ -70,3 +70,10 @@ tag: tag-push: just tag git push origin tag v$(just tag_version) + +# get commit messages for a release +release-notes: + git log --pretty=format:"- %s" v$(just tag_version)..HEAD + +langfuse-server: + ./scripts/setup_langfuse.sh diff --git a/packages/exchange/.env.langfuse.local b/packages/exchange/.env.langfuse.local new file mode 100644 index 000000000..cdebcd7a1 --- /dev/null +++ b/packages/exchange/.env.langfuse.local @@ -0,0 +1,16 @@ +# These variables are default initialization variables for locally hosted Langfuse server +LANGFUSE_INIT_PROJECT_NAME=goose-local +LANGFUSE_INIT_PROJECT_PUBLIC_KEY=publickey-local +LANGFUSE_INIT_PROJECT_SECRET_KEY=secretkey-local +LANGFUSE_INIT_USER_EMAIL=local@block.xyz +LANGFUSE_INIT_USER_NAME=localdev +LANGFUSE_INIT_USER_PASSWORD=localpwd + +LANGFUSE_INIT_ORG_ID=local-id +LANGFUSE_INIT_ORG_NAME=local-org +LANGFUSE_INIT_PROJECT_ID=goose + +# These variables are used by Goose +LANGFUSE_PUBLIC_KEY=publickey-local +LANGFUSE_SECRET_KEY=secretkey-local +LANGFUSE_HOST=http://localhost:3000 diff --git a/packages/exchange/pyproject.toml b/packages/exchange/pyproject.toml index 83a9e3c25..a25782f9f 100644 --- a/packages/exchange/pyproject.toml +++ b/packages/exchange/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ai-exchange" -version = "0.9.3" +version = "0.9.5" description = "a uniform python SDK for message generation with LLMs" readme = "README.md" requires-python = ">=3.10" @@ -13,6 +13,8 @@ dependencies = [ "tiktoken>=0.7.0", "httpx>=0.27.0", "tenacity>=9.0.0", + "python-dotenv>=1.0.1", + "langfuse>=2.38.2" ] [tool.hatch.build.targets.wheel] @@ -33,6 +35,7 @@ anthropic = "exchange.providers.anthropic:AnthropicProvider" bedrock = "exchange.providers.bedrock:BedrockProvider" ollama = "exchange.providers.ollama:OllamaProvider" google = "exchange.providers.google:GoogleProvider" +groq = "exchange.providers.groq:GroqProvider" [project.entry-points."exchange.moderator"] passive = "exchange.moderators.passive:PassiveModerator" diff --git a/packages/exchange/src/exchange/checkpoint.py b/packages/exchange/src/exchange/checkpoint.py index f355dd0a2..063ef35d1 100644 --- a/packages/exchange/src/exchange/checkpoint.py +++ b/packages/exchange/src/exchange/checkpoint.py @@ -1,5 +1,4 @@ from copy import deepcopy -from typing import List from attrs import define, field @@ -31,7 +30,7 @@ class CheckpointData: total_token_count: int = field(default=0) # in order list of individual checkpoints in the exchange - checkpoints: List[Checkpoint] = field(factory=list) + checkpoints: list[Checkpoint] = field(factory=list) # the offset to apply to the message index when calculating the last message index # this is useful because messages on the exchange behave like a queue, where you can only diff --git a/packages/exchange/src/exchange/content.py b/packages/exchange/src/exchange/content.py index b9cc986fc..66957b7c6 100644 --- a/packages/exchange/src/exchange/content.py +++ b/packages/exchange/src/exchange/content.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional +from typing import Optional from attrs import define, asdict @@ -7,11 +7,11 @@ class Content: - def __init_subclass__(cls, **kwargs: Dict[str, Any]) -> None: + def __init_subclass__(cls, **kwargs: dict[str, any]) -> None: super().__init_subclass__(**kwargs) CONTENT_TYPES[cls.__name__] = cls - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, any]: data = asdict(self, recurse=True) data["type"] = self.__class__.__name__ return data @@ -26,7 +26,7 @@ class Text(Content): class ToolUse(Content): id: str name: str - parameters: Any + parameters: any is_error: bool = False error_message: Optional[str] = None diff --git a/packages/exchange/src/exchange/exchange.py b/packages/exchange/src/exchange/exchange.py index b2fdbc5ec..942bf78c6 100644 --- a/packages/exchange/src/exchange/exchange.py +++ b/packages/exchange/src/exchange/exchange.py @@ -1,9 +1,9 @@ import json import traceback from copy import deepcopy -from typing import Any, Dict, List, Mapping, Tuple - +from typing import Mapping from attrs import define, evolve, field, Factory +from exchange.langfuse_wrapper import observe_wrapper from tiktoken import get_encoding from exchange.checkpoint import Checkpoint, CheckpointData @@ -41,8 +41,8 @@ class Exchange: model: str system: str moderator: Moderator = field(default=ContextTruncate()) - tools: Tuple[Tool] = field(factory=tuple, converter=tuple) - messages: List[Message] = field(factory=list) + tools: tuple[Tool, ...] = field(factory=tuple, converter=tuple) + messages: list[Message] = field(factory=list) checkpoint_data: CheckpointData = field(factory=CheckpointData) generation_args: dict = field(default=Factory(dict)) @@ -50,7 +50,7 @@ class Exchange: def _toolmap(self) -> Mapping[str, Tool]: return {tool.name: tool for tool in self.tools} - def replace(self, **kwargs: Dict[str, Any]) -> "Exchange": + def replace(self, **kwargs: dict[str, any]) -> "Exchange": """Make a copy of the exchange, replacing any passed arguments""" # TODO: ensure that the checkpoint data is updated correctly. aka, # if we replace the messages, we need to update the checkpoint data @@ -127,6 +127,7 @@ def reply(self, max_tool_use: int = 128) -> Message: return response + @observe_wrapper() def call_function(self, tool_use: ToolUse) -> ToolResult: """Call the function indicated by the tool use""" tool = self._toolmap.get(tool_use.name) @@ -264,7 +265,7 @@ def pop_first_message(self) -> Message: # we've removed all the checkpoints, so we need to reset the message index offset self.checkpoint_data.message_index_offset = 0 - def pop_last_checkpoint(self) -> Tuple[Checkpoint, List[Message]]: + def pop_last_checkpoint(self) -> tuple[Checkpoint, list[Message]]: """ Reverts the exchange back to the last checkpoint, removing associated messages """ @@ -275,7 +276,7 @@ def pop_last_checkpoint(self) -> Tuple[Checkpoint, List[Message]]: messages.append(self.messages.pop()) return removed_checkpoint, messages - def pop_first_checkpoint(self) -> Tuple[Checkpoint, List[Message]]: + def pop_first_checkpoint(self) -> tuple[Checkpoint, list[Message]]: """ Pop the first checkpoint from the exchange, removing associated messages """ @@ -332,5 +333,6 @@ def is_allowed_to_call_llm(self) -> bool: # this to be a required method of the provider instead. return len(self.messages) > 0 and self.messages[-1].role == "user" - def get_token_usage(self) -> Dict[str, Usage]: + @staticmethod + def get_token_usage() -> dict[str, Usage]: return _token_usage_collector.get_token_usage_group_by_model() diff --git a/packages/exchange/src/exchange/invalid_choice_error.py b/packages/exchange/src/exchange/invalid_choice_error.py index ffbb9899f..def35bbc0 100644 --- a/packages/exchange/src/exchange/invalid_choice_error.py +++ b/packages/exchange/src/exchange/invalid_choice_error.py @@ -1,8 +1,5 @@ -from typing import List - - class InvalidChoiceError(Exception): - def __init__(self, attribute_name: str, attribute_value: str, available_values: List[str]) -> None: + def __init__(self, attribute_name: str, attribute_value: str, available_values: list[str]) -> None: self.attribute_name = attribute_name self.attribute_value = attribute_value self.available_values = available_values diff --git a/packages/exchange/src/exchange/langfuse_wrapper.py b/packages/exchange/src/exchange/langfuse_wrapper.py new file mode 100644 index 000000000..c8cec23ee --- /dev/null +++ b/packages/exchange/src/exchange/langfuse_wrapper.py @@ -0,0 +1,84 @@ +""" +Langfuse Integration Module + +This module provides integration with Langfuse, a tool for monitoring and tracing LLM applications. + +Usage: + Import this module to enable Langfuse integration. + It automatically checks for Langfuse credentials in the .env.langfuse file and for a running Langfuse server. + If these are found, it will set up the necessary client and context for tracing. + +Note: + Run setup_langfuse.sh which automates the steps for running local Langfuse. +""" + +import os +from typing import Callable +from dotenv import load_dotenv +from langfuse.decorators import langfuse_context +import sys +from io import StringIO +from pathlib import Path +from functools import wraps # Add this import + + +def find_package_root(start_path: Path, marker_file: str = "pyproject.toml") -> Path: + while start_path != start_path.parent: + if (start_path / marker_file).exists(): + return start_path + start_path = start_path.parent + return None + + +def auth_check() -> bool: + # Temporarily redirect stdout and stderr to suppress print statements from Langfuse + temp_stderr = StringIO() + sys.stderr = temp_stderr + + # Load environment variables + load_dotenv(LANGFUSE_ENV_FILE, override=True) + + auth_val = langfuse_context.auth_check() + + # Restore stderr + sys.stderr = sys.__stderr__ + return auth_val + + +CURRENT_DIR = Path(__file__).parent +PACKAGE_ROOT = find_package_root(CURRENT_DIR) + +LANGFUSE_ENV_FILE = os.path.join(PACKAGE_ROOT, ".env.langfuse.local") +HAS_LANGFUSE_CREDENTIALS = False +load_dotenv(LANGFUSE_ENV_FILE, override=True) + +HAS_LANGFUSE_CREDENTIALS = auth_check() + + +def observe_wrapper(*args, **kwargs) -> Callable: # noqa + """ + A decorator that wraps a function with Langfuse context observation if credentials are available. + + If Langfuse credentials were found, the function will be wrapped with Langfuse's observe method. + Otherwise, the function will be returned as-is. + + Args: + *args: Positional arguments to pass to langfuse_context.observe. + **kwargs: Keyword arguments to pass to langfuse_context.observe. + + Returns: + Callable: The wrapped function if credentials are available, otherwise the original function. + """ + + def _wrapper(fn: Callable) -> Callable: + if HAS_LANGFUSE_CREDENTIALS: + + @wraps(fn) + def wrapped_fn(*fargs, **fkwargs): # noqa + return langfuse_context.observe(*args, **kwargs)(fn)(*fargs, **fkwargs) + + return wrapped_fn + else: + return fn + + return _wrapper diff --git a/packages/exchange/src/exchange/message.py b/packages/exchange/src/exchange/message.py index 035c60345..5edff692c 100644 --- a/packages/exchange/src/exchange/message.py +++ b/packages/exchange/src/exchange/message.py @@ -1,7 +1,7 @@ import inspect import time from pathlib import Path -from typing import Any, Dict, List, Literal, Type +from typing import Literal from attrs import define, field from jinja2 import Environment, FileSystemLoader @@ -12,7 +12,7 @@ Role = Literal["user", "assistant"] -def validate_role_and_content(instance: "Message", *_: Any) -> None: # noqa: ANN401 +def validate_role_and_content(instance: "Message", *_: any) -> None: # noqa: ANN401 if instance.role == "user": if not (instance.text or instance.tool_result): raise ValueError("User message must include a Text or ToolResult") @@ -25,7 +25,7 @@ def validate_role_and_content(instance: "Message", *_: Any) -> None: # noqa: AN raise ValueError("Assistant message does not support ToolResult") -def content_converter(contents: List[Dict[str, Any]]) -> List[Content]: +def content_converter(contents: list[dict[str, any]]) -> list[Content]: return [(CONTENT_TYPES[c.pop("type")](**c) if c.__class__ not in CONTENT_TYPES.values() else c) for c in contents] @@ -48,9 +48,9 @@ class Message: role: Role = field(default="user") id: str = field(factory=lambda: str(create_object_id(prefix="msg"))) created: int = field(factory=lambda: int(time.time())) - content: List[Content] = field(factory=list, validator=validate_role_and_content, converter=content_converter) + content: list[Content] = field(factory=list, validator=validate_role_and_content, converter=content_converter) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, any]: return { "role": self.role, "id": self.id, @@ -68,7 +68,7 @@ def text(self) -> str: return "\n".join(result) @property - def tool_use(self) -> List[ToolUse]: + def tool_use(self) -> list[ToolUse]: """All tool use content of this message.""" result = [] for content in self.content: @@ -77,7 +77,7 @@ def tool_use(self) -> List[ToolUse]: return result @property - def tool_result(self) -> List[ToolResult]: + def tool_result(self) -> list[ToolResult]: """All tool result content of this message.""" result = [] for content in self.content: @@ -87,10 +87,10 @@ def tool_result(self) -> List[ToolResult]: @classmethod def load( - cls: Type["Message"], + cls: type["Message"], filename: str, role: Role = "user", - **kwargs: Dict[str, Any], + **kwargs: dict[str, any], ) -> "Message": """Load the message from filename relative to where the load is called. @@ -113,9 +113,9 @@ def load( return cls(role=role, content=[Text(text=rendered_content)]) @classmethod - def user(cls: Type["Message"], text: str) -> "Message": + def user(cls: type["Message"], text: str) -> "Message": return cls(role="user", content=[Text(text)]) @classmethod - def assistant(cls: Type["Message"], text: str) -> "Message": + def assistant(cls: type["Message"], text: str) -> "Message": return cls(role="assistant", content=[Text(text)]) diff --git a/packages/exchange/src/exchange/moderators/__init__.py b/packages/exchange/src/exchange/moderators/__init__.py index 82d032e42..925473e98 100644 --- a/packages/exchange/src/exchange/moderators/__init__.py +++ b/packages/exchange/src/exchange/moderators/__init__.py @@ -1,5 +1,4 @@ from functools import cache -from typing import Type from exchange.invalid_choice_error import InvalidChoiceError from exchange.moderators.base import Moderator @@ -10,7 +9,7 @@ @cache -def get_moderator(name: str) -> Type[Moderator]: +def get_moderator(name: str) -> type[Moderator]: moderators = load_plugins(group="exchange.moderator") if name not in moderators: raise InvalidChoiceError("moderator", name, moderators.keys()) diff --git a/packages/exchange/src/exchange/moderators/base.py b/packages/exchange/src/exchange/moderators/base.py index d7c630c6a..98a6ad663 100644 --- a/packages/exchange/src/exchange/moderators/base.py +++ b/packages/exchange/src/exchange/moderators/base.py @@ -1,8 +1,7 @@ from abc import ABC, abstractmethod -from typing import Type class Moderator(ABC): @abstractmethod - def rewrite(self, exchange: Type["exchange.exchange.Exchange"]) -> None: # noqa: F821 + def rewrite(self, exchange: type["exchange.exchange.Exchange"]) -> None: # noqa: F821 pass diff --git a/packages/exchange/src/exchange/moderators/passive.py b/packages/exchange/src/exchange/moderators/passive.py index e3a24efbd..30e6f2c66 100644 --- a/packages/exchange/src/exchange/moderators/passive.py +++ b/packages/exchange/src/exchange/moderators/passive.py @@ -1,7 +1,6 @@ -from typing import Type from exchange.moderators.base import Moderator class PassiveModerator(Moderator): - def rewrite(self, _: Type["exchange.exchange.Exchange"]) -> None: # noqa: F821 + def rewrite(self, _: type["exchange.exchange.Exchange"]) -> None: # noqa: F821 pass diff --git a/packages/exchange/src/exchange/moderators/summarizer.py b/packages/exchange/src/exchange/moderators/summarizer.py index 7e2dd5588..a7bb1b0f5 100644 --- a/packages/exchange/src/exchange/moderators/summarizer.py +++ b/packages/exchange/src/exchange/moderators/summarizer.py @@ -1,12 +1,10 @@ -from typing import Type - from exchange import Message from exchange.checkpoint import CheckpointData from exchange.moderators import ContextTruncate, PassiveModerator class ContextSummarizer(ContextTruncate): - def rewrite(self, exchange: Type["exchange.exchange.Exchange"]) -> None: # noqa: F821 + def rewrite(self, exchange: type["exchange.exchange.Exchange"]) -> None: # noqa: F821 """Summarize the context history up to the last few messages in the exchange""" self._update_system_prompt_token_count(exchange) diff --git a/packages/exchange/src/exchange/moderators/truncate.py b/packages/exchange/src/exchange/moderators/truncate.py index 41115f663..a9c08b650 100644 --- a/packages/exchange/src/exchange/moderators/truncate.py +++ b/packages/exchange/src/exchange/moderators/truncate.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Optional from exchange.checkpoint import CheckpointData from exchange.message import Message @@ -62,7 +62,7 @@ def _update_system_prompt_token_count(self, exchange: Exchange) -> None: exchange.checkpoint_data.total_token_count -= last_system_prompt_token_count exchange.checkpoint_data.total_token_count += self.system_prompt_token_count - def _get_messages_to_remove(self, exchange: Exchange) -> List[Message]: + def _get_messages_to_remove(self, exchange: Exchange) -> list[Message]: # this keeps all the messages/checkpoints throwaway_exchange = exchange.replace( moderator=PassiveModerator(), diff --git a/packages/exchange/src/exchange/providers/__init__.py b/packages/exchange/src/exchange/providers/__init__.py index f92d4f769..56418df47 100644 --- a/packages/exchange/src/exchange/providers/__init__.py +++ b/packages/exchange/src/exchange/providers/__init__.py @@ -1,5 +1,4 @@ from functools import cache -from typing import Type from exchange.invalid_choice_error import InvalidChoiceError from exchange.providers.anthropic import AnthropicProvider # noqa @@ -7,6 +6,7 @@ from exchange.providers.databricks import DatabricksProvider # noqa from exchange.providers.openai import OpenAiProvider # noqa from exchange.providers.ollama import OllamaProvider # noqa +from exchange.providers.groq import GroqProvider # noqa from exchange.providers.azure import AzureProvider # noqa from exchange.providers.google import GoogleProvider # noqa @@ -14,7 +14,7 @@ @cache -def get_provider(name: str) -> Type[Provider]: +def get_provider(name: str) -> type[Provider]: providers = load_plugins(group="exchange.provider") if name not in providers: raise InvalidChoiceError("provider", name, providers.keys()) diff --git a/packages/exchange/src/exchange/providers/anthropic.py b/packages/exchange/src/exchange/providers/anthropic.py index 84ecd12fb..c98c6d432 100644 --- a/packages/exchange/src/exchange/providers/anthropic.py +++ b/packages/exchange/src/exchange/providers/anthropic.py @@ -1,5 +1,4 @@ import os -from typing import Any, Dict, List, Tuple, Type import httpx @@ -8,6 +7,7 @@ from exchange.providers.base import Provider, Usage from tenacity import retry, wait_fixed, stop_after_attempt from exchange.providers.utils import retry_if_status, raise_for_status +from exchange.langfuse_wrapper import observe_wrapper ANTHROPIC_HOST = "https://api.anthropic.com/v1/messages" @@ -29,7 +29,7 @@ def __init__(self, client: httpx.Client) -> None: self.client = client @classmethod - def from_env(cls: Type["AnthropicProvider"]) -> "AnthropicProvider": + def from_env(cls: type["AnthropicProvider"]) -> "AnthropicProvider": cls.check_env_vars() url = os.environ.get("ANTHROPIC_HOST", ANTHROPIC_HOST) key = os.environ.get("ANTHROPIC_API_KEY") @@ -45,7 +45,7 @@ def from_env(cls: Type["AnthropicProvider"]) -> "AnthropicProvider": return cls(client) @staticmethod - def get_usage(data: Dict) -> Usage: # noqa: ANN401 + def get_usage(data: dict) -> Usage: # noqa: ANN401 usage = data.get("usage") input_tokens = usage.get("input_tokens") output_tokens = usage.get("output_tokens") @@ -61,7 +61,7 @@ def get_usage(data: Dict) -> Usage: # noqa: ANN401 ) @staticmethod - def anthropic_response_to_message(response: Dict) -> Message: + def anthropic_response_to_message(response: dict) -> Message: content_blocks = response.get("content", []) content = [] for block in content_blocks: @@ -78,7 +78,7 @@ def anthropic_response_to_message(response: Dict) -> Message: return Message(role="assistant", content=content) @staticmethod - def tools_to_anthropic_spec(tools: Tuple[Tool]) -> List[Dict[str, Any]]: + def tools_to_anthropic_spec(tools: tuple[Tool, ...]) -> list[dict[str, any]]: return [ { "name": tool.name, @@ -89,7 +89,7 @@ def tools_to_anthropic_spec(tools: Tuple[Tool]) -> List[Dict[str, Any]]: ] @staticmethod - def messages_to_anthropic_spec(messages: List[Message]) -> List[Dict[str, Any]]: + def messages_to_anthropic_spec(messages: list[Message]) -> list[dict[str, any]]: messages_spec = [] # if messages is empty - just make a default for message in messages: @@ -123,14 +123,17 @@ def messages_to_anthropic_spec(messages: List[Message]) -> List[Dict[str, Any]]: messages_spec.append(converted) return messages_spec + @observe_wrapper(as_type="generation") def complete( self, model: str, system: str, - messages: List[Message], - tools: List[Tool] = [], - **kwargs: Dict[str, Any], - ) -> Tuple[Message, Usage]: + messages: list[Message], + tools: list[Tool] = None, + **kwargs: dict[str, any], + ) -> tuple[Message, Usage]: + if tools is None: + tools = [] tools_set = set() unique_tools = [] for tool in tools: diff --git a/packages/exchange/src/exchange/providers/azure.py b/packages/exchange/src/exchange/providers/azure.py index 4d470f978..fa8814f39 100644 --- a/packages/exchange/src/exchange/providers/azure.py +++ b/packages/exchange/src/exchange/providers/azure.py @@ -1,5 +1,3 @@ -from typing import Type - import httpx import os @@ -21,7 +19,7 @@ def __init__(self, client: httpx.Client) -> None: super().__init__(client) @classmethod - def from_env(cls: Type["AzureProvider"]) -> "AzureProvider": + def from_env(cls: type["AzureProvider"]) -> "AzureProvider": cls.check_env_vars() url = os.environ.get("AZURE_CHAT_COMPLETIONS_HOST_NAME") deployment_name = os.environ.get("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME") diff --git a/packages/exchange/src/exchange/providers/base.py b/packages/exchange/src/exchange/providers/base.py index c8d860ecc..76b1c3391 100644 --- a/packages/exchange/src/exchange/providers/base.py +++ b/packages/exchange/src/exchange/providers/base.py @@ -1,7 +1,7 @@ import os from abc import ABC, abstractmethod from attrs import define, field -from typing import List, Optional, Tuple, Type +from typing import Optional from exchange.message import Message from exchange.tool import Tool @@ -19,11 +19,11 @@ class Provider(ABC): REQUIRED_ENV_VARS: list[str] = [] @classmethod - def from_env(cls: Type["Provider"]) -> "Provider": + def from_env(cls: type["Provider"]) -> "Provider": return cls() @classmethod - def check_env_vars(cls: Type["Provider"], instructions_url: Optional[str] = None) -> None: + def check_env_vars(cls: type["Provider"], instructions_url: Optional[str] = None) -> None: for env_var in cls.REQUIRED_ENV_VARS: if env_var not in os.environ: raise MissingProviderEnvVariableError(env_var, cls.PROVIDER_NAME, instructions_url) @@ -33,9 +33,10 @@ def complete( self, model: str, system: str, - messages: List[Message], - tools: Tuple[Tool], - ) -> Tuple[Message, Usage]: + messages: list[Message], + tools: tuple[Tool, ...], + **kwargs: dict[str, any], + ) -> tuple[Message, Usage]: """Generate the next message using the specified model""" pass diff --git a/packages/exchange/src/exchange/providers/bedrock.py b/packages/exchange/src/exchange/providers/bedrock.py index 6c32d7cb3..cdc0c29c9 100644 --- a/packages/exchange/src/exchange/providers/bedrock.py +++ b/packages/exchange/src/exchange/providers/bedrock.py @@ -4,7 +4,7 @@ import logging import os from datetime import datetime, timezone -from typing import Any, Dict, List, Optional, Tuple, Type +from typing import Optional from urllib.parse import quote, urlparse import httpx @@ -15,6 +15,7 @@ from tenacity import retry, wait_fixed, stop_after_attempt from exchange.providers.utils import raise_for_status, retry_if_status from exchange.tool import Tool +from exchange.langfuse_wrapper import observe_wrapper SERVICE = "bedrock-runtime" UTC = timezone.utc @@ -36,7 +37,7 @@ def __init__( aws_access_key: str, aws_secret_key: str, aws_session_token: Optional[str] = None, - **kwargs: Dict[str, Any], + **kwargs: dict[str, any], ) -> None: self.region = aws_region self.host = f"https://{SERVICE}.{aws_region}.amazonaws.com/" @@ -45,7 +46,7 @@ def __init__( self.session_token = aws_session_token super().__init__(base_url=self.host, timeout=600, **kwargs) - def post(self, path: str, json: Dict, **kwargs: Dict[str, Any]) -> httpx.Response: + def post(self, path: str, json: dict, **kwargs: dict[str, any]) -> httpx.Response: signed_headers = self.sign_and_get_headers( method="POST", url=path, @@ -60,7 +61,7 @@ def sign_and_get_headers( url: str, payload: dict, service: str, - ) -> Dict[str, str]: + ) -> dict[str, str]: """ Sign the request and generate the necessary headers for AWS authentication. @@ -72,10 +73,10 @@ def sign_and_get_headers( region (str): The AWS region. access_key (str): The AWS access key. secret_key (str): The AWS secret key. - session_token (Optional[str]): The AWS session token, if any. + session_token (optional[str]): The AWS session token, if any. Returns: - Dict[str, str]: The headers required for the request. + dict[str, str]: The headers required for the request. """ def sign(key: bytes, msg: str) -> bytes: @@ -160,7 +161,7 @@ def __init__(self, client: AwsClient) -> None: self.client = client @classmethod - def from_env(cls: Type["BedrockProvider"]) -> "BedrockProvider": + def from_env(cls: type["BedrockProvider"]) -> "BedrockProvider": cls.check_env_vars() aws_region = os.environ.get("AWS_REGION", "us-east-1") aws_access_key = os.environ.get("AWS_ACCESS_KEY_ID") @@ -175,26 +176,27 @@ def from_env(cls: Type["BedrockProvider"]) -> "BedrockProvider": ) return cls(client=client) + @observe_wrapper(as_type="generation") def complete( self, model: str, system: str, - messages: List[Message], - tools: Tuple[Tool], - **kwargs: Dict[str, Any], - ) -> Tuple[Message, Usage]: + messages: list[Message], + tools: tuple[Tool, ...], + **kwargs: dict[str, any], + ) -> tuple[Message, Usage]: """ Generate a completion response from the Bedrock gateway. Args: model (str): The model identifier. system (str): The system prompt or configuration. - messages (List[Message]): A list of messages to be processed by the model. - tools (Tuple[Tool]): A tuple of tools to be used in the completion process. + messages (list[Message]): A list of messages to be processed by the model. + tools (tuple[Tool]): A tuple of tools to be used in the completion process. **kwargs: Additional keyword arguments for inference configuration. Returns: - Tuple[Message, Usage]: A tuple containing the response message and usage data. + tuple[Message, Usage]: A tuple containing the response message and usage data. """ inference_config = dict( @@ -231,7 +233,7 @@ def complete( return self.response_to_message(response_message), usage @retry_procedure - def _post(self, payload: Any, path: str) -> dict: # noqa: ANN401 + def _post(self, payload: any, path: str) -> dict: # noqa: ANN401 response = self.client.post(path, json=payload) return raise_for_status(response).json() @@ -311,7 +313,7 @@ def response_to_message(response_message: dict) -> Message: raise Exception("Invalid response") @staticmethod - def tools_to_bedrock_spec(tools: Tuple[Tool]) -> Optional[dict]: + def tools_to_bedrock_spec(tools: tuple[Tool, ...]) -> Optional[dict]: if len(tools) == 0: return None # API requires a non-empty tool config or None tools_added = set() diff --git a/packages/exchange/src/exchange/providers/databricks.py b/packages/exchange/src/exchange/providers/databricks.py index 9bd582dc5..b8f92dca9 100644 --- a/packages/exchange/src/exchange/providers/databricks.py +++ b/packages/exchange/src/exchange/providers/databricks.py @@ -1,5 +1,3 @@ -from typing import Any, Dict, List, Tuple, Type - import httpx import os @@ -13,7 +11,7 @@ tools_to_openai_spec, ) from exchange.tool import Tool - +from exchange.langfuse_wrapper import observe_wrapper retry_procedure = retry( wait=wait_fixed(2), @@ -43,7 +41,7 @@ def __init__(self, client: httpx.Client) -> None: self.client = client @classmethod - def from_env(cls: Type["DatabricksProvider"]) -> "DatabricksProvider": + def from_env(cls: type["DatabricksProvider"]) -> "DatabricksProvider": cls.check_env_vars(cls.instructions_url) url = os.environ.get("DATABRICKS_HOST") key = os.environ.get("DATABRICKS_TOKEN") @@ -69,14 +67,15 @@ def get_usage(data: dict) -> Usage: total_tokens=total_tokens, ) + @observe_wrapper(as_type="generation") def complete( self, model: str, system: str, - messages: List[Message], - tools: Tuple[Tool], - **kwargs: Dict[str, Any], - ) -> Tuple[Message, Usage]: + messages: list[Message], + tools: tuple[Tool, ...], + **kwargs: dict[str, any], + ) -> tuple[Message, Usage]: payload = dict( messages=[ {"role": "system", "content": system}, diff --git a/packages/exchange/src/exchange/providers/google.py b/packages/exchange/src/exchange/providers/google.py index fe83cd605..76ccd7a9c 100644 --- a/packages/exchange/src/exchange/providers/google.py +++ b/packages/exchange/src/exchange/providers/google.py @@ -1,5 +1,4 @@ import os -from typing import Any, Dict, List, Tuple, Type import httpx @@ -7,7 +6,9 @@ from exchange.content import Text, ToolResult, ToolUse from exchange.providers.base import Provider, Usage from tenacity import retry, wait_fixed, stop_after_attempt -from exchange.providers.utils import raise_for_status, retry_if_status +from exchange.providers.utils import raise_for_status, retry_if_status, encode_image +from exchange.langfuse_wrapper import observe_wrapper + GOOGLE_HOST = "https://generativelanguage.googleapis.com/v1beta" @@ -30,7 +31,7 @@ def __init__(self, client: httpx.Client) -> None: self.client = client @classmethod - def from_env(cls: Type["GoogleProvider"]) -> "GoogleProvider": + def from_env(cls: type["GoogleProvider"]) -> "GoogleProvider": cls.check_env_vars(cls.instructions_url) url = os.environ.get("GOOGLE_HOST", GOOGLE_HOST) key = os.environ.get("GOOGLE_API_KEY") @@ -45,7 +46,7 @@ def from_env(cls: Type["GoogleProvider"]) -> "GoogleProvider": return cls(client) @staticmethod - def get_usage(data: Dict) -> Usage: # noqa: ANN401 + def get_usage(data: dict) -> Usage: # noqa: ANN401 usage = data.get("usageMetadata") input_tokens = usage.get("promptTokenCount") output_tokens = usage.get("candidatesTokenCount") @@ -61,7 +62,7 @@ def get_usage(data: Dict) -> Usage: # noqa: ANN401 ) @staticmethod - def google_response_to_message(response: Dict) -> Message: + def google_response_to_message(response: dict) -> Message: candidates = response.get("candidates", []) if candidates: # Only use first candidate for now @@ -85,12 +86,12 @@ def google_response_to_message(response: Dict) -> Message: return Message(role="assistant", content=[]) @staticmethod - def tools_to_google_spec(tools: Tuple[Tool]) -> Dict[str, List[Dict[str, Any]]]: + def tools_to_google_spec(tools: tuple[Tool, ...]) -> dict[str, list[dict[str, any]]]: if not tools: return {} converted_tools = [] for tool in tools: - converted_tool: Dict[str, Any] = { + converted_tool: dict[str, any] = { "name": tool.name, "description": tool.description or "", } @@ -100,7 +101,7 @@ def tools_to_google_spec(tools: Tuple[Tool]) -> Dict[str, List[Dict[str, Any]]]: return {"functionDeclarations": converted_tools} @staticmethod - def messages_to_google_spec(messages: List[Message]) -> List[Dict[str, Any]]: + def messages_to_google_spec(messages: list[Message]) -> list[dict[str, any]]: messages_spec = [] for message in messages: role = "user" if message.role == "user" else "model" @@ -111,9 +112,20 @@ def messages_to_google_spec(messages: List[Message]) -> List[Dict[str, Any]]: elif isinstance(content, ToolUse): converted["parts"].append({"functionCall": {"name": content.name, "args": content.parameters}}) elif isinstance(content, ToolResult): - converted["parts"].append( - {"functionResponse": {"name": content.tool_use_id, "response": {"content": content.output}}} - ) + if content.output.startswith('"image:'): + image_path = content.output.replace('"image:', "").replace('"', "") + converted["parts"].append( + { + "inline_data": { + "mime_type": "image/png", + "data": f"{encode_image(image_path)}", + } + } + ) + else: + converted["parts"].append( + {"functionResponse": {"name": content.tool_use_id, "response": {"content": content.output}}} + ) messages_spec.append(converted) if not messages_spec: @@ -121,14 +133,15 @@ def messages_to_google_spec(messages: List[Message]) -> List[Dict[str, Any]]: return messages_spec + @observe_wrapper(as_type="generation") def complete( self, model: str, system: str, - messages: List[Message], - tools: List[Tool] = [], - **kwargs: Dict[str, Any], - ) -> Tuple[Message, Usage]: + messages: list[Message], + tools: list[Tool] = None, + **kwargs: dict[str, any], + ) -> tuple[Message, Usage]: tools_set = set() unique_tools = [] for tool in tools: diff --git a/packages/exchange/src/exchange/providers/groq.py b/packages/exchange/src/exchange/providers/groq.py new file mode 100644 index 000000000..0f6472f88 --- /dev/null +++ b/packages/exchange/src/exchange/providers/groq.py @@ -0,0 +1,98 @@ +import os + +from exchange.langfuse_wrapper import observe_wrapper +import httpx + +from exchange.message import Message +from exchange.providers.base import Provider, Usage +from exchange.providers.utils import ( + messages_to_openai_spec, + openai_response_to_message, + openai_single_message_context_length_exceeded, + raise_for_status, + tools_to_openai_spec, +) +from exchange.tool import Tool +from tenacity import retry, wait_fixed, stop_after_attempt +from exchange.providers.utils import retry_if_status + +GROQ_HOST = "https://api.groq.com/openai/" + +retry_procedure = retry( + wait=wait_fixed(5), + stop=stop_after_attempt(5), + retry=retry_if_status(codes=[429], above=500), + reraise=True, +) + + +class GroqProvider(Provider): + """Provides chat completions for models hosted directly by OpenAI.""" + + PROVIDER_NAME = "groq" + REQUIRED_ENV_VARS = ["GROQ_API_KEY"] + instructions_url = "https://console.groq.com/docs/quickstart" + + def __init__(self, client: httpx.Client) -> None: + self.client = client + + @classmethod + def from_env(cls: type["GroqProvider"]) -> "GroqProvider": + cls.check_env_vars(cls.instructions_url) + url = os.environ.get("GROQ_HOST", GROQ_HOST) + key = os.environ.get("GROQ_API_KEY") + + client = httpx.Client( + base_url=url + "v1/", + headers={"Authorization": "Bearer " + key}, + timeout=httpx.Timeout(60 * 10), + ) + return cls(client) + + @staticmethod + def get_usage(data: dict) -> Usage: + usage = data.pop("usage") + input_tokens = usage.get("prompt_tokens") + output_tokens = usage.get("completion_tokens") + total_tokens = usage.get("total_tokens") + + if total_tokens is None and input_tokens is not None and output_tokens is not None: + total_tokens = input_tokens + output_tokens + + return Usage( + input_tokens=input_tokens, + output_tokens=output_tokens, + total_tokens=total_tokens, + ) + + @observe_wrapper(as_type="generation") + def complete( + self, + model: str, + system: str, + messages: list[Message], + tools: tuple[Tool, ...], + **kwargs: dict[str, any], + ) -> tuple[Message, Usage]: + system_message = [{"role": "system", "content": system}] + payload = dict( + messages=system_message + messages_to_openai_spec(messages), + model=model, + tools=tools_to_openai_spec(tools) if tools else [], + **kwargs, + ) + payload = {k: v for k, v in payload.items() if v} + response = self._post(payload) + + # Check for context_length_exceeded error for single, long input message + if "error" in response and len(messages) == 1: + openai_single_message_context_length_exceeded(response["error"]) + + message = openai_response_to_message(response) + usage = self.get_usage(response) + return message, usage + + @retry_procedure + def _post(self, payload: dict) -> dict: + response = self.client.post("chat/completions", json=payload) + return raise_for_status(response).json() diff --git a/packages/exchange/src/exchange/providers/ollama.py b/packages/exchange/src/exchange/providers/ollama.py index 888564640..51fef5105 100644 --- a/packages/exchange/src/exchange/providers/ollama.py +++ b/packages/exchange/src/exchange/providers/ollama.py @@ -1,29 +1,28 @@ import os -from typing import Type import httpx from exchange.providers.openai import OpenAiProvider OLLAMA_HOST = "http://localhost:11434/" -OLLAMA_MODEL = "mistral-nemo" +OLLAMA_MODEL = "qwen2.5" class OllamaProvider(OpenAiProvider): """Provides chat completions for models hosted by Ollama.""" - __doc__ += """Here's an example profile configuration to try: + __doc__ += f"""Here's an example profile configuration to try: First run: ollama pull qwen2.5, then use this profile: ollama: provider: ollama - processor: qwen2.5 - accelerator: qwen2.5 + processor: {OLLAMA_MODEL} + accelerator: {OLLAMA_MODEL} moderator: truncate toolkits: - name: developer - requires: {} + requires: {{}} """ def __init__(self, client: httpx.Client) -> None: @@ -31,7 +30,7 @@ def __init__(self, client: httpx.Client) -> None: super().__init__(client) @classmethod - def from_env(cls: Type["OllamaProvider"]) -> "OllamaProvider": + def from_env(cls: type["OllamaProvider"]) -> "OllamaProvider": ollama_url = os.environ.get("OLLAMA_HOST", OLLAMA_HOST) timeout = httpx.Timeout(60 * 10) diff --git a/packages/exchange/src/exchange/providers/openai.py b/packages/exchange/src/exchange/providers/openai.py index b25c5a70a..8701e5429 100644 --- a/packages/exchange/src/exchange/providers/openai.py +++ b/packages/exchange/src/exchange/providers/openai.py @@ -1,5 +1,4 @@ import os -from typing import Any, Dict, List, Tuple, Type import httpx @@ -15,6 +14,7 @@ from exchange.tool import Tool from tenacity import retry, wait_fixed, stop_after_attempt from exchange.providers.utils import retry_if_status +from exchange.langfuse_wrapper import observe_wrapper OPENAI_HOST = "https://api.openai.com/" @@ -37,7 +37,7 @@ def __init__(self, client: httpx.Client) -> None: self.client = client @classmethod - def from_env(cls: Type["OpenAiProvider"]) -> "OpenAiProvider": + def from_env(cls: type["OpenAiProvider"]) -> "OpenAiProvider": cls.check_env_vars(cls.instructions_url) url = os.environ.get("OPENAI_HOST", OPENAI_HOST) key = os.environ.get("OPENAI_API_KEY") @@ -65,14 +65,15 @@ def get_usage(data: dict) -> Usage: total_tokens=total_tokens, ) + @observe_wrapper(as_type="generation") def complete( self, model: str, system: str, - messages: List[Message], - tools: Tuple[Tool], - **kwargs: Dict[str, Any], - ) -> Tuple[Message, Usage]: + messages: list[Message], + tools: tuple[Tool, ...], + **kwargs: dict[str, any], + ) -> tuple[Message, Usage]: system_message = [] if model.startswith("o1") else [{"role": "system", "content": system}] payload = dict( messages=system_message + messages_to_openai_spec(messages), diff --git a/packages/exchange/src/exchange/providers/utils.py b/packages/exchange/src/exchange/providers/utils.py index 4be7ac31e..9af7287ef 100644 --- a/packages/exchange/src/exchange/providers/utils.py +++ b/packages/exchange/src/exchange/providers/utils.py @@ -1,7 +1,7 @@ import base64 import json import re -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Optional import httpx from exchange.content import Text, ToolResult, ToolUse @@ -10,10 +10,10 @@ from tenacity import retry_if_exception -def retry_if_status(codes: Optional[List[int]] = None, above: Optional[int] = None) -> Callable: +def retry_if_status(codes: Optional[list[int]] = None, above: Optional[int] = None) -> callable: codes = codes or [] - def predicate(exc: Exception) -> bool: + def predicate(exc: BaseException) -> bool: if isinstance(exc, httpx.HTTPStatusError): if exc.response.status_code in codes: return True @@ -42,7 +42,7 @@ def encode_image(image_path: str) -> str: return base64.b64encode(image_file.read()).decode("utf-8") -def messages_to_openai_spec(messages: List[Message]) -> List[Dict[str, Any]]: +def messages_to_openai_spec(messages: list[Message]) -> list[dict[str, any]]: messages_spec = [] for message in messages: converted = {"role": message.role} @@ -106,7 +106,7 @@ def messages_to_openai_spec(messages: List[Message]) -> List[Dict[str, Any]]: return messages_spec -def tools_to_openai_spec(tools: Tuple[Tool]) -> Dict[str, Any]: +def tools_to_openai_spec(tools: tuple[Tool, ...]) -> dict[str, any]: tools_names = set() result = [] for tool in tools: diff --git a/packages/exchange/src/exchange/token_usage_collector.py b/packages/exchange/src/exchange/token_usage_collector.py index 8f0801062..c99110c29 100644 --- a/packages/exchange/src/exchange/token_usage_collector.py +++ b/packages/exchange/src/exchange/token_usage_collector.py @@ -1,5 +1,4 @@ from collections import defaultdict -from typing import Dict from exchange.providers.base import Usage @@ -11,7 +10,7 @@ def __init__(self) -> None: def collect(self, model: str, usage: Usage) -> None: self.usage_data.append((model, usage)) - def get_token_usage_group_by_model(self) -> Dict[str, Usage]: + def get_token_usage_group_by_model(self) -> dict[str, Usage]: usage_group_by_model = defaultdict(lambda: Usage(0, 0, 0)) for model, usage in self.usage_data: usage_by_model = usage_group_by_model[model] diff --git a/packages/exchange/src/exchange/tool.py b/packages/exchange/src/exchange/tool.py index 4ce9e7c50..1ca1f4358 100644 --- a/packages/exchange/src/exchange/tool.py +++ b/packages/exchange/src/exchange/tool.py @@ -1,5 +1,4 @@ import inspect -from typing import Any, Callable, Type from attrs import define @@ -13,17 +12,17 @@ class Tool: Attributes: name (str): The name of the tool description (str): A description of what the tool does - parameters dict[str, Any]: A json schema of the function signature + parameters dict[str, any]: A json schema of the function signature function (Callable): The python function that powers the tool """ name: str description: str - parameters: dict[str, Any] - function: Callable + parameters: dict[str, any] + function: callable @classmethod - def from_function(cls: Type["Tool"], func: Any) -> "Tool": # noqa: ANN401 + def from_function(cls: type["Tool"], func: any) -> "Tool": # noqa: ANN401 """Create a tool instance from a function and its docstring The function must have a docstring - we require it to load the description diff --git a/packages/exchange/src/exchange/utils.py b/packages/exchange/src/exchange/utils.py index 04d5ffa18..b95f1c485 100644 --- a/packages/exchange/src/exchange/utils.py +++ b/packages/exchange/src/exchange/utils.py @@ -1,7 +1,7 @@ import inspect import uuid from importlib.metadata import entry_points -from typing import Any, Callable, Dict, List, Type, get_args, get_origin +from typing import get_args, get_origin from griffe import ( Docstring, @@ -20,7 +20,7 @@ def compact(content: str) -> str: return " ".join(content.split()) -def parse_docstring(func: Callable) -> tuple[str, List[Dict]]: +def parse_docstring(func: callable) -> tuple[str, list[dict]]: """Get description and parameters from function docstring""" function_args = list(inspect.signature(func).parameters.keys()) text = str(func.__doc__) @@ -71,7 +71,7 @@ def parse_docstring(func: Callable) -> tuple[str, List[Dict]]: def _check_section_is_present( - parsed_docstring: List[DocstringSection], section_type: Type[DocstringSectionText] + parsed_docstring: list[DocstringSection], section_type: type[DocstringSectionText] ) -> bool: for section in parsed_docstring: if isinstance(section, section_type): @@ -79,7 +79,7 @@ def _check_section_is_present( return False -def json_schema(func: Any) -> dict[str, Any]: # noqa: ANN401 +def json_schema(func: any) -> dict[str, any]: # noqa: ANN401 """Get the json schema for a function""" signature = inspect.signature(func) parameters = signature.parameters @@ -107,16 +107,16 @@ def json_schema(func: Any) -> dict[str, Any]: # noqa: ANN401 return schema -def _map_type_to_schema(py_type: Type) -> Dict[str, Any]: # noqa: ANN401 +def _map_type_to_schema(py_type: type) -> dict[str, any]: # noqa: ANN401 origin = get_origin(py_type) args = get_args(py_type) if origin is list or origin is tuple: - return {"type": "array", "items": _map_type_to_schema(args[0] if args else Any)} + return {"type": "array", "items": _map_type_to_schema(args[0] if args else any)} elif origin is dict: return { "type": "object", - "additionalProperties": _map_type_to_schema(args[1] if len(args) > 1 else Any), + "additionalProperties": _map_type_to_schema(args[1] if len(args) > 1 else any), } elif py_type is int: return {"type": "integer"} diff --git a/packages/exchange/tests/providers/cassettes/test_azure_complete.yaml b/packages/exchange/tests/providers/cassettes/test_azure_complete.yaml index 3ac8a4fc0..8d7a34239 100644 --- a/packages/exchange/tests/providers/cassettes/test_azure_complete.yaml +++ b/packages/exchange/tests/providers/cassettes/test_azure_complete.yaml @@ -1,7 +1,19 @@ interactions: - request: - body: '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Hello"}], "model": "gpt-4o-mini"}' + body: |- + { + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "Hello" + } + ], + "model": "gpt-4o-mini" + } headers: accept: - '*/*' @@ -23,35 +35,94 @@ interactions: uri: https://test.openai.azure.com/openai/deployments/test-azure-deployment/chat/completions?api-version=2024-05-01-preview response: body: - string: '{"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":"stop","index":0,"logprobs":null,"message":{"content":"Hello! - How can I assist you today?","role":"assistant"}}],"created":1727230065,"id":"chatcmpl-ABBjN3AoYlxkP7Vg2lBvUhYeA6j5K","model":"gpt-4-32k","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}],"system_fingerprint":null,"usage":{"completion_tokens":9,"prompt_tokens":18,"total_tokens":27}} - - ' + string: |- + { + "choices": [ + { + "content_filter_results": { + "hate": { + "filtered": false, + "severity": "safe" + }, + "self_harm": { + "filtered": false, + "severity": "safe" + }, + "sexual": { + "filtered": false, + "severity": "safe" + }, + "violence": { + "filtered": false, + "severity": "safe" + } + }, + "finish_reason": "stop", + "index": 0, + "logprobs": null, + "message": { + "content": "Hello! How can I assist you today?\n", + "role": "assistant" + } + } + ], + "created": 1728788170, + "id": "chatcmpl-AHj469gGa9bQaCSikrZYDejfGDx2x", + "model": "gpt-4-32k", + "object": "chat.completion", + "prompt_filter_results": [ + { + "prompt_index": 0, + "content_filter_results": { + "hate": { + "filtered": false, + "severity": "safe" + }, + "self_harm": { + "filtered": false, + "severity": "safe" + }, + "sexual": { + "filtered": false, + "severity": "safe" + }, + "violence": { + "filtered": false, + "severity": "safe" + } + } + } + ], + "system_fingerprint": null, + "usage": { + "completion_tokens": 9, + "prompt_tokens": 18, + "total_tokens": 27 + } + } headers: Cache-Control: - no-cache, must-revalidate Content-Length: - - '825' + - '827' Content-Type: - application/json Date: - - Wed, 25 Sep 2024 02:07:45 GMT - Set-Cookie: test_set_cookie + - Sun, 13 Oct 2024 02:56:10 GMT Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload access-control-allow-origin: - '*' apim-request-id: - - 82e66ef8-ac07-4a43-b60f-9aecec1d8c81 + - b203f657-e7aa-40ea-8969-ba0b83dae854 azureml-model-session: - - d145-20240919052126 - openai-organization: test_openai_org_key + - d156-20241010120317 x-accel-buffering: - 'no' x-content-type-options: - nosniff x-ms-client-request-id: - - 82e66ef8-ac07-4a43-b60f-9aecec1d8c81 + - b203f657-e7aa-40ea-8969-ba0b83dae854 x-ms-rai-invoked: - 'true' x-ms-region: @@ -61,7 +132,7 @@ interactions: x-ratelimit-remaining-tokens: - '79984' x-request-id: - - 38db9001-8b16-4efe-84c9-620e10f18c3c + - 0c6dc92f-a017-4879-b82e-be937533c76e status: code: 200 message: OK diff --git a/packages/exchange/tests/providers/cassettes/test_azure_tools.yaml b/packages/exchange/tests/providers/cassettes/test_azure_tools.yaml index 9da479790..240b83bf4 100644 --- a/packages/exchange/tests/providers/cassettes/test_azure_tools.yaml +++ b/packages/exchange/tests/providers/cassettes/test_azure_tools.yaml @@ -1,13 +1,40 @@ interactions: - request: - body: '{"messages": [{"role": "system", "content": "You are a helpful assistant. - Expect to need to read a file using read_file."}, {"role": "user", "content": - "What are the contents of this file? test.txt"}], "model": "gpt-4o-mini", "tools": - [{"type": "function", "function": {"name": "read_file", "description": "Read - the contents of the file.", "parameters": {"type": "object", "properties": {"filename": - {"type": "string", "description": "The path to the file, which can be relative - or\nabsolute. If it is a plain filename, it is assumed to be in the\ncurrent - working directory."}}, "required": ["filename"]}}}]}' + body: |- + { + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant. Expect to need to read a file using read_file." + }, + { + "role": "user", + "content": "What are the contents of this file? test.txt" + } + ], + "model": "gpt-4o-mini", + "tools": [ + { + "type": "function", + "function": { + "name": "read_file", + "description": "Read the contents of the file.", + "parameters": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "description": "The path to the file, which can be relative or\nabsolute. If it is a plain filename, it is assumed to be in the\ncurrent working directory." + } + }, + "required": [ + "filename" + ] + } + } + } + ] + } headers: accept: - '*/*' @@ -29,10 +56,64 @@ interactions: uri: https://test.openai.azure.com/openai/deployments/test-azure-deployment/chat/completions?api-version=2024-05-01-preview response: body: - string: '{"choices":[{"content_filter_results":{},"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"content":null,"role":"assistant","tool_calls":[{"function":{"arguments":"{\n \"filename\": - \"test.txt\"\n}","name":"read_file"},"id":"call_a47abadDxlGKIWjvYYvGVAHa","type":"function"}]}}],"created":1727256650,"id":"chatcmpl-ABIeABbq5WVCq0e0AriGFaYDSih3P","model":"gpt-4-32k","object":"chat.completion","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}],"system_fingerprint":null,"usage":{"completion_tokens":16,"prompt_tokens":109,"total_tokens":125}} - - ' + string: |- + { + "choices": [ + { + "content_filter_results": {}, + "finish_reason": "tool_calls", + "index": 0, + "logprobs": null, + "message": { + "content": null, + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\n \"filename\": \"test.txt\"\n}", + "name": "read_file" + }, + "id": "call_Dn0idyNSdmHSYpsql3EbFH9L", + "type": "function" + } + ] + } + } + ], + "created": 1728788173, + "id": "chatcmpl-AHj498XEBkixukw2lwNReXCIBStp0", + "model": "gpt-4-32k", + "object": "chat.completion", + "prompt_filter_results": [ + { + "prompt_index": 0, + "content_filter_results": { + "hate": { + "filtered": false, + "severity": "safe" + }, + "self_harm": { + "filtered": false, + "severity": "safe" + }, + "sexual": { + "filtered": false, + "severity": "safe" + }, + "violence": { + "filtered": false, + "severity": "safe" + } + } + } + ], + "system_fingerprint": null, + "usage": { + "completion_tokens": 16, + "prompt_tokens": 109, + "total_tokens": 125 + } + } headers: Cache-Control: - no-cache, must-revalidate @@ -41,33 +122,31 @@ interactions: Content-Type: - application/json Date: - - Wed, 25 Sep 2024 09:30:50 GMT - Set-Cookie: test_set_cookie + - Sun, 13 Oct 2024 02:56:14 GMT Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload access-control-allow-origin: - '*' apim-request-id: - - 8c0e3372-8ffd-4ff5-a5d1-0b962c4ea339 + - 0e0af575-5634-415c-88e3-a7bf68549ee5 azureml-model-session: - - d145-20240919052126 - openai-organization: test_openai_org_key + - d159-20241010142543 x-accel-buffering: - 'no' x-content-type-options: - nosniff x-ms-client-request-id: - - 8c0e3372-8ffd-4ff5-a5d1-0b962c4ea339 + - 0e0af575-5634-415c-88e3-a7bf68549ee5 x-ms-rai-invoked: - 'true' x-ms-region: - Switzerland North x-ratelimit-remaining-requests: - - '79' + - '77' x-ratelimit-remaining-tokens: - - '79824' + - '79952' x-request-id: - - 401bd803-b790-47b7-b098-98708d44f060 + - e5012889-ef86-449a-908c-2065dbf0954e status: code: 200 message: OK diff --git a/packages/exchange/tests/providers/cassettes/test_google_complete.yaml b/packages/exchange/tests/providers/cassettes/test_google_complete.yaml new file mode 100644 index 000000000..ec01cc8e0 --- /dev/null +++ b/packages/exchange/tests/providers/cassettes/test_google_complete.yaml @@ -0,0 +1,112 @@ +interactions: +- request: + body: |- + { + "system_instruction": { + "parts": [ + { + "text": "You are a helpful assistant." + } + ] + }, + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "Hello" + } + ] + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '139' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + user-agent: + - python-httpx/0.27.2 + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=test_google_api_key + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Hello! \ud83d\udc4b What can I do for you today? \ud83d\ude0a \n" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 8, + "candidatesTokenCount": 13, + "totalTokenCount": 21 + } + } + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sun, 13 Oct 2024 02:54:58 GMT + Server: + - scaffolding on HTTPServer2 + Server-Timing: + - gfet4t7; dur=1201 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '858' + status: + code: 200 + message: OK +version: 1 diff --git a/packages/exchange/tests/providers/cassettes/test_google_tools.yaml b/packages/exchange/tests/providers/cassettes/test_google_tools.yaml new file mode 100644 index 000000000..c50e86810 --- /dev/null +++ b/packages/exchange/tests/providers/cassettes/test_google_tools.yaml @@ -0,0 +1,137 @@ +interactions: +- request: + body: |- + { + "system_instruction": { + "parts": [ + { + "text": "You are a helpful assistant. Expect to need to read a file using read_file." + } + ] + }, + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "What are the contents of this file? test.txt" + } + ] + } + ], + "tools": { + "functionDeclarations": [ + { + "name": "read_file", + "description": "Read the contents of the file.", + "parameters": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "description": "The path to the file, which can be relative or\nabsolute. If it is a plain filename, it is assumed to be in the\ncurrent working directory." + } + }, + "required": [ + "filename" + ] + } + } + ] + } + } + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '600' + content-type: + - application/json + host: + - generativelanguage.googleapis.com + user-agent: + - python-httpx/0.27.2 + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=test_google_api_key + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "read_file", + "args": { + "filename": "test.txt" + } + } + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 101, + "candidatesTokenCount": 17, + "totalTokenCount": 118 + } + } + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sun, 13 Oct 2024 02:54:59 GMT + Server: + - scaffolding on HTTPServer2 + Server-Timing: + - gfet4t7; dur=449 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '947' + status: + code: 200 + message: OK +version: 1 diff --git a/packages/exchange/tests/providers/cassettes/test_ollama_complete.yaml b/packages/exchange/tests/providers/cassettes/test_ollama_complete.yaml index 88bc206ff..84afcbe99 100644 --- a/packages/exchange/tests/providers/cassettes/test_ollama_complete.yaml +++ b/packages/exchange/tests/providers/cassettes/test_ollama_complete.yaml @@ -23,15 +23,25 @@ interactions: Content-Type: - text/plain; charset=utf-8 Date: - - Sun, 22 Sep 2024 23:40:13 GMT - Set-Cookie: test_set_cookie - openai-organization: test_openai_org_key + - Sun, 13 Oct 2024 04:53:22 GMT status: code: 200 message: OK - request: - body: '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Hello"}], "model": "mistral-nemo"}' + body: |- + { + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "Hello" + } + ], + "model": "mistral-nemo" + } headers: accept: - '*/*' @@ -51,17 +61,36 @@ interactions: uri: http://localhost:11434/v1/chat/completions response: body: - string: "{\"id\":\"chatcmpl-429\",\"object\":\"chat.completion\",\"created\":1727048416,\"model\":\"mistral-nemo\",\"system_fingerprint\":\"fp_ollama\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"Hello! - I'm here to help. How can I assist you today? Let's chat. \U0001F60A\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":23,\"total_tokens\":33}}\n" + string: |- + { + "id": "chatcmpl-565", + "object": "chat.completion", + "created": 1728795204, + "model": "mistral-nemo", + "system_fingerprint": "fp_ollama", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! I'm here to help. How can I assist you today? \ud83d\ude0a" + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 19, + "total_tokens": 29 + } + } headers: Content-Length: - - '356' + - '344' Content-Type: - application/json Date: - - Sun, 22 Sep 2024 23:40:16 GMT - Set-Cookie: test_set_cookie - openai-organization: test_openai_org_key + - Sun, 13 Oct 2024 04:53:24 GMT status: code: 200 message: OK diff --git a/packages/exchange/tests/providers/cassettes/test_ollama_tools.yaml b/packages/exchange/tests/providers/cassettes/test_ollama_tools.yaml index 7271bf227..d803153b8 100644 --- a/packages/exchange/tests/providers/cassettes/test_ollama_tools.yaml +++ b/packages/exchange/tests/providers/cassettes/test_ollama_tools.yaml @@ -23,21 +23,46 @@ interactions: Content-Type: - text/plain; charset=utf-8 Date: - - Wed, 25 Sep 2024 09:23:08 GMT - Set-Cookie: test_set_cookie - openai-organization: test_openai_org_key + - Sun, 13 Oct 2024 02:52:00 GMT status: code: 200 message: OK - request: - body: '{"messages": [{"role": "system", "content": "You are a helpful assistant. - Expect to need to read a file using read_file."}, {"role": "user", "content": - "What are the contents of this file? test.txt"}], "model": "mistral-nemo", "tools": - [{"type": "function", "function": {"name": "read_file", "description": "Read - the contents of the file.", "parameters": {"type": "object", "properties": {"filename": - {"type": "string", "description": "The path to the file, which can be relative - or\nabsolute. If it is a plain filename, it is assumed to be in the\ncurrent - working directory."}}, "required": ["filename"]}}}]}' + body: |- + { + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant. Expect to need to read a file using read_file." + }, + { + "role": "user", + "content": "What are the contents of this file? test.txt" + } + ], + "model": "mistral-nemo", + "tools": [ + { + "type": "function", + "function": { + "name": "read_file", + "description": "Read the contents of the file.", + "parameters": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "description": "The path to the file, which can be relative or\nabsolute. If it is a plain filename, it is assumed to be in the\ncurrent working directory." + } + }, + "required": [ + "filename" + ] + } + } + } + ] + } headers: accept: - '*/*' @@ -57,18 +82,46 @@ interactions: uri: http://localhost:11434/v1/chat/completions response: body: - string: '{"id":"chatcmpl-245","object":"chat.completion","created":1727256190,"model":"mistral-nemo","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"","tool_calls":[{"id":"call_z6fgu3z3","type":"function","function":{"name":"read_file","arguments":"{\"filename\":\"test.txt\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":112,"completion_tokens":21,"total_tokens":133}} - - ' + string: |- + { + "id": "chatcmpl-212", + "object": "chat.completion", + "created": 1728787922, + "model": "mistral-nemo", + "system_fingerprint": "fp_ollama", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_h5d3s25w", + "type": "function", + "function": { + "name": "read_file", + "arguments": "{\"filename\":\"test.txt\"}" + } + } + ] + }, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 112, + "completion_tokens": 21, + "total_tokens": 133 + } + } headers: Content-Length: - '425' Content-Type: - application/json Date: - - Wed, 25 Sep 2024 09:23:10 GMT - Set-Cookie: test_set_cookie - openai-organization: test_openai_org_key + - Sun, 13 Oct 2024 02:52:02 GMT status: code: 200 message: OK diff --git a/packages/exchange/tests/providers/cassettes/test_openai_complete.yaml b/packages/exchange/tests/providers/cassettes/test_openai_complete.yaml index 1a92eb36b..6de10fb5d 100644 --- a/packages/exchange/tests/providers/cassettes/test_openai_complete.yaml +++ b/packages/exchange/tests/providers/cassettes/test_openai_complete.yaml @@ -1,7 +1,19 @@ interactions: - request: - body: '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Hello"}], "model": "gpt-4o-mini"}' + body: |- + { + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "Hello" + } + ], + "model": "gpt-4o-mini" + } headers: accept: - '*/*' @@ -23,25 +35,48 @@ interactions: uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-AAQTYi3DXJnltAfd5sUH1Wnzh69t3\",\n \"object\": - \"chat.completion\",\n \"created\": 1727048416,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n - \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": \"Hello! How can I assist you today?\",\n - \ \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\": - \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 18,\n \"completion_tokens\": - 9,\n \"total_tokens\": 27,\n \"completion_tokens_details\": {\n \"reasoning_tokens\": - 0\n }\n },\n \"system_fingerprint\": \"fp_1bb46167f9\"\n}\n" + string: |- + { + "id": "chatcmpl-AHj1Y1xN9345uFT3PVMInIYEQ8g4a", + "object": "chat.completion", + "created": 1728788012, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?", + "refusal": null + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 18, + "completion_tokens": 9, + "total_tokens": 27, + "prompt_tokens_details": { + "cached_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0 + } + }, + "system_fingerprint": "fp_e2bde53e6e" + } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - - 8c762399feb55739-SYD + - 8d1c0a328dca3e44-SIN Connection: - keep-alive Content-Type: - application/json Date: - - Sun, 22 Sep 2024 23:40:17 GMT + - Sun, 13 Oct 2024 02:53:33 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -51,15 +86,17 @@ interactions: - nosniff access-control-expose-headers: - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 content-length: - - '593' + - '656' openai-organization: test_openai_org_key openai-processing-ms: - - '560' + - '481' openai-version: - '2020-10-01' strict-transport-security: - - max-age=15552000; includeSubDomains; preload + - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: @@ -67,13 +104,13 @@ interactions: x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - - '199973' + - '199972' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 8ms x-request-id: - - req_22e26c840219cde3152eaba1ce89483b + - req_85f532ac5fdad6a4af020cab55e2fd4d status: code: 200 message: OK diff --git a/packages/exchange/tests/providers/cassettes/test_openai_tools.yaml b/packages/exchange/tests/providers/cassettes/test_openai_tools.yaml index 30496fcb8..86c6962c3 100644 --- a/packages/exchange/tests/providers/cassettes/test_openai_tools.yaml +++ b/packages/exchange/tests/providers/cassettes/test_openai_tools.yaml @@ -1,13 +1,40 @@ interactions: - request: - body: '{"messages": [{"role": "system", "content": "You are a helpful assistant. - Expect to need to read a file using read_file."}, {"role": "user", "content": - "What are the contents of this file? test.txt"}], "model": "gpt-4o-mini", "tools": - [{"type": "function", "function": {"name": "read_file", "description": "Read - the contents of the file.", "parameters": {"type": "object", "properties": {"filename": - {"type": "string", "description": "The path to the file, which can be relative - or\nabsolute. If it is a plain filename, it is assumed to be in the\ncurrent - working directory."}}, "required": ["filename"]}}}]}' + body: |- + { + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant. Expect to need to read a file using read_file." + }, + { + "role": "user", + "content": "What are the contents of this file? test.txt" + } + ], + "model": "gpt-4o-mini", + "tools": [ + { + "type": "function", + "function": { + "name": "read_file", + "description": "Read the contents of the file.", + "parameters": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "description": "The path to the file, which can be relative or\nabsolute. If it is a plain filename, it is assumed to be in the\ncurrent working directory." + } + }, + "required": [ + "filename" + ] + } + } + } + ] + } headers: accept: - '*/*' @@ -29,29 +56,58 @@ interactions: uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-ABIV2aZWVKQ774RAQ8KHYdNwkI5N7\",\n \"object\": - \"chat.completion\",\n \"created\": 1727256084,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n - \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n - \ \"id\": \"call_xXYlw4A7Ud1qtCopuK5gEJrP\",\n \"type\": - \"function\",\n \"function\": {\n \"name\": \"read_file\",\n - \ \"arguments\": \"{\\\"filename\\\":\\\"test.txt\\\"}\"\n }\n - \ }\n ],\n \"refusal\": null\n },\n \"logprobs\": - null,\n \"finish_reason\": \"tool_calls\"\n }\n ],\n \"usage\": - {\n \"prompt_tokens\": 107,\n \"completion_tokens\": 15,\n \"total_tokens\": - 122,\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0\n - \ }\n },\n \"system_fingerprint\": \"fp_1bb46167f9\"\n}\n" + string: |- + { + "id": "chatcmpl-AHj1axEdpe3coVDULrCjHmXql5euz", + "object": "chat.completion", + "created": 1728788014, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_Z43oz2RtLmNHw9xvFgxA1SC5", + "type": "function", + "function": { + "name": "read_file", + "arguments": "{\"filename\":\"test.txt\"}" + } + } + ], + "refusal": null + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 107, + "completion_tokens": 15, + "total_tokens": 122, + "prompt_tokens_details": { + "cached_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0 + } + }, + "system_fingerprint": "fp_e2bde53e6e" + } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - - 8c89f19fed997e43-SYD + - 8d1c0a419b009d0b-SIN Connection: - keep-alive Content-Type: - application/json Date: - - Wed, 25 Sep 2024 09:21:25 GMT + - Sun, 13 Oct 2024 02:53:35 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -61,11 +117,13 @@ interactions: - nosniff access-control-expose-headers: - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 content-length: - - '844' + - '907' openai-organization: test_openai_org_key openai-processing-ms: - - '266' + - '442' openai-version: - '2020-10-01' strict-transport-security: @@ -75,15 +133,15 @@ interactions: x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - - '9991' + - '9997' x-ratelimit-remaining-tokens: - - '199952' + - '199951' x-ratelimit-reset-requests: - - 1m9.486s + - 23.873s x-ratelimit-reset-tokens: - 14ms x-request-id: - - req_ff6b5d65c24f40e1faaf049c175e718d + - req_8ec455e318c9f2d6eecf82d1fdf124ab status: code: 200 message: OK diff --git a/packages/exchange/tests/providers/cassettes/test_openai_vision.yaml b/packages/exchange/tests/providers/cassettes/test_openai_vision.yaml index 1b9691d29..c649d6804 100644 --- a/packages/exchange/tests/providers/cassettes/test_openai_vision.yaml +++ b/packages/exchange/tests/providers/cassettes/test_openai_vision.yaml @@ -1,13 +1,53 @@ interactions: - request: - body: '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "What does the first entry in the menu say?"}, {"role": - "assistant", "tool_calls": [{"id": "xyz", "type": "function", "function": {"name": - "screenshot", "arguments": "{}"}}]}, {"role": "tool", "content": [{"type": "text", - "text": "This tool result included an image that is uploaded in the next message."}], - "tool_call_id": "xyz"}, {"role": "user", "content": [{"type": "image_url", "image_url": - {"url": "data:image/jpeg;base64,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"}}]}], - "model": "gpt-4o-mini"}' + body: |- + { + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "What does the first entry in the menu say?" + }, + { + "role": "assistant", + "tool_calls": [ + { + "id": "xyz", + "type": "function", + "function": { + "name": "screenshot", + "arguments": "{}" + } + } + ] + }, + { + "role": "tool", + "content": [ + { + "type": "text", + "text": "This tool result included an image that is uploaded in the next message." + } + ], + "tool_call_id": "xyz" + }, + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": { + "url": "data:image/jpeg;base64,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" + } + } + ] + } + ], + "model": "gpt-4o-mini" + } headers: accept: - '*/*' @@ -29,25 +69,48 @@ interactions: uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-ABIA0YzOHlhqb02K8Ay4Jwsw6xOpk\",\n \"object\": - \"chat.completion\",\n \"created\": 1727254780,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n - \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": \"The first entry in the menu says \\\"Ask - Goose.\\\"\",\n \"refusal\": null\n },\n \"logprobs\": null,\n - \ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": - 14230,\n \"completion_tokens\": 11,\n \"total_tokens\": 14241,\n \"completion_tokens_details\": - {\n \"reasoning_tokens\": 0\n }\n },\n \"system_fingerprint\": \"fp_e9627b5346\"\n}\n" + string: |- + { + "id": "chatcmpl-AHj1wBv5ZKIB2nh2p8PWvLZ3QEXLH", + "object": "chat.completion", + "created": 1728788036, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The first entry in the menu says \"Ask Goose.\"", + "refusal": null + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 14230, + "completion_tokens": 11, + "total_tokens": 14241, + "prompt_tokens_details": { + "cached_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0 + } + }, + "system_fingerprint": "fp_8552ec53e1" + } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - - 8c89d1c45d98a883-SYD + - 8d1c0abdef9701f2-SIN Connection: - keep-alive Content-Type: - application/json Date: - - Wed, 25 Sep 2024 08:59:41 GMT + - Sun, 13 Oct 2024 02:53:57 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -57,11 +120,13 @@ interactions: - nosniff access-control-expose-headers: - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 content-length: - - '613' + - '676' openai-organization: test_openai_org_key openai-processing-ms: - - '1289' + - '1966' openai-version: - '2020-10-01' strict-transport-security: @@ -71,15 +136,15 @@ interactions: x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - - '9999' + - '9995' x-ratelimit-remaining-tokens: - '199177' x-ratelimit-reset-requests: - - 8.64s + - 37.664s x-ratelimit-reset-tokens: - 246ms x-request-id: - - req_9503b21e31db78c4ebd2b71b304cea72 + - req_6c0595ef0498819df0c77a9ada75a8e5 status: code: 200 message: OK diff --git a/packages/exchange/tests/providers/conftest.py b/packages/exchange/tests/providers/conftest.py index 010504e84..a747e9ce6 100644 --- a/packages/exchange/tests/providers/conftest.py +++ b/packages/exchange/tests/providers/conftest.py @@ -1,11 +1,13 @@ +import json import os import re -from typing import Type, Tuple import pytest +import yaml from exchange import Message, ToolUse, ToolResult, Tool from exchange.providers import Usage, Provider + from tests.conftest import read_file OPENAI_API_KEY = "test_openai_api_key" @@ -53,6 +55,90 @@ def default_azure_env(monkeypatch): monkeypatch.setenv("AZURE_CHAT_COMPLETIONS_KEY", AZURE_API_KEY) +GOOGLE_API_KEY = "test_google_api_key" + + +@pytest.fixture +def default_google_env(monkeypatch): + """ + This fixture prevents GoogleProvider.from_env() from erring on missing + environment variables. + + When running VCR tests for the first time or after deleting a cassette + recording, set required environment variables, so that real requests don't + fail. Subsequent runs use the recorded data, so don't need them. + """ + if "GOOGLE_API_KEY" not in os.environ: + monkeypatch.setenv("GOOGLE_API_KEY", GOOGLE_API_KEY) + + +class LiteralBlockScalar(str): + """Formats the string as a literal block scalar, preserving whitespace and + without interpreting escape characters""" + + pass + + +def literal_block_scalar_presenter(dumper, data): + """Represents a scalar string as a literal block, via '|' syntax""" + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") + + +yaml.add_representer(LiteralBlockScalar, literal_block_scalar_presenter) + + +def process_string_value(string_value): + """Pretty-prints JSON or returns long strings as a LiteralString""" + try: + json_data = json.loads(string_value) + return LiteralBlockScalar(json.dumps(json_data, indent=2)) + except (ValueError, TypeError): + if len(string_value) > 80: + return LiteralBlockScalar(string_value) + return string_value + + +def convert_body_to_literal(data): + """Searches the data for body strings, attempting to pretty-print JSON""" + if isinstance(data, dict): + for key, value in data.items(): + # Handle response body case (e.g., response.body.string) + if key == "body" and isinstance(value, dict) and "string" in value: + value["string"] = process_string_value(value["string"]) + + # Handle request body case (e.g., request.body) + elif key == "body" and isinstance(value, str): + data[key] = process_string_value(value) + + else: + convert_body_to_literal(value) + + elif isinstance(data, list): + for i, item in enumerate(data): + data[i] = convert_body_to_literal(item) + + return data + + +class PrettyPrintJSONBody: + """This makes request and response body recordings more readable.""" + + @staticmethod + def serialize(cassette_dict): + cassette_dict = convert_body_to_literal(cassette_dict) + return yaml.dump(cassette_dict, default_flow_style=False, allow_unicode=True) + + @staticmethod + def deserialize(cassette_string): + return yaml.load(cassette_string, Loader=yaml.Loader) + + +@pytest.fixture(scope="module") +def vcr(vcr): + vcr.register_serializer("yaml", PrettyPrintJSONBody) + return vcr + + @pytest.fixture(scope="module") def vcr_config(): """ @@ -85,6 +171,8 @@ def scrub_request_url(request): request.uri = re.sub(r"/deployments/[^/]+", f"/deployments/{AZURE_DEPLOYMENT_NAME}", request.uri) request.headers["host"] = AZURE_ENDPOINT.replace("https://", "") request.headers["api-key"] = AZURE_API_KEY + elif "generativelanguage.googleapis.com" in request.uri: + request.uri = re.sub(r"([?&])key=[^&]+", r"\1key=" + GOOGLE_API_KEY, request.uri) return request @@ -93,19 +181,21 @@ def scrub_response_headers(response): """ This scrubs sensitive response headers. Note they are case-sensitive! """ - response["headers"]["openai-organization"] = OPENAI_ORG_ID - response["headers"]["Set-Cookie"] = "test_set_cookie" + if "openai-organization" in response["headers"]: + response["headers"]["openai-organization"] = OPENAI_ORG_ID + if "Set-Cookie" in response["headers"]: + response["headers"]["Set-Cookie"] = "test_set_cookie" return response -def complete(provider_cls: Type[Provider], model: str, **kwargs) -> Tuple[Message, Usage]: +def complete(provider_cls: type[Provider], model: str, **kwargs) -> tuple[Message, Usage]: provider = provider_cls.from_env() system = "You are a helpful assistant." messages = [Message.user("Hello")] - return provider.complete(model=model, system=system, messages=messages, tools=None, **kwargs) + return provider.complete(model=model, system=system, messages=messages, tools=(), **kwargs) -def tools(provider_cls: Type[Provider], model: str, **kwargs) -> Tuple[Message, Usage]: +def tools(provider_cls: type[Provider], model: str, **kwargs) -> tuple[Message, Usage]: provider = provider_cls.from_env() system = "You are a helpful assistant. Expect to need to read a file using read_file." messages = [Message.user("What are the contents of this file? test.txt")] @@ -114,7 +204,7 @@ def tools(provider_cls: Type[Provider], model: str, **kwargs) -> Tuple[Message, ) -def vision(provider_cls: Type[Provider], model: str, **kwargs) -> Tuple[Message, Usage]: +def vision(provider_cls: type[Provider], model: str, **kwargs) -> tuple[Message, Usage]: provider = provider_cls.from_env() system = "You are a helpful assistant." messages = [ @@ -128,4 +218,4 @@ def vision(provider_cls: Type[Provider], model: str, **kwargs) -> Tuple[Message, content=[ToolResult(tool_use_id="xyz", output='"image:tests/test_image.png"')], ), ] - return provider.complete(model=model, system=system, messages=messages, tools=None, **kwargs) + return provider.complete(model=model, system=system, messages=messages, tools=(), **kwargs) diff --git a/packages/exchange/tests/providers/test_anthropic.py b/packages/exchange/tests/providers/test_anthropic.py index 272ebcb0f..daf925270 100644 --- a/packages/exchange/tests/providers/test_anthropic.py +++ b/packages/exchange/tests/providers/test_anthropic.py @@ -91,7 +91,7 @@ def test_message_text_to_anthropic_spec() -> None: def test_messages_to_anthropic_spec() -> None: messages = [ - Message(role="user", content=[Text(text="Hello, Claude")]), + Message(role="user", content=[Text("Hello, Claude")]), Message( role="assistant", content=[ToolUse(id="1", name="example_fn", parameters={"param": "value"})], @@ -148,7 +148,7 @@ def create_response(status_code, json_data=None): reply_message, reply_usage = anthropic_provider.complete(model=model, system=system, messages=messages) - assert reply_message.content == [Text(text="Hello from Claude!")] + assert reply_message.content == [Text("Hello from Claude!")] assert reply_usage.total_tokens == 35 assert mock_post.call_count == 2 mock_post.assert_any_call( diff --git a/packages/exchange/tests/providers/test_azure.py b/packages/exchange/tests/providers/test_azure.py index b46be30b9..44b75d380 100644 --- a/packages/exchange/tests/providers/test_azure.py +++ b/packages/exchange/tests/providers/test_azure.py @@ -14,10 +14,10 @@ @pytest.mark.parametrize( "env_var_name", [ - ("AZURE_CHAT_COMPLETIONS_HOST_NAME"), - ("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME"), - ("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_API_VERSION"), - ("AZURE_CHAT_COMPLETIONS_KEY"), + "AZURE_CHAT_COMPLETIONS_HOST_NAME", + "AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME", + "AZURE_CHAT_COMPLETIONS_DEPLOYMENT_API_VERSION", + "AZURE_CHAT_COMPLETIONS_KEY", ], ) def test_from_env_throw_error_when_missing_env_var(env_var_name): @@ -43,7 +43,7 @@ def test_from_env_throw_error_when_missing_env_var(env_var_name): def test_azure_complete(default_azure_env): reply_message, reply_usage = complete(AzureProvider, AZURE_MODEL) - assert reply_message.content == [Text(text="Hello! How can I assist you today?")] + assert reply_message.content == [Text("Hello! How can I assist you today?\n")] assert reply_usage.total_tokens == 27 @@ -61,7 +61,7 @@ def test_azure_tools(default_azure_env): tool_use = reply_message.content[0] assert isinstance(tool_use, ToolUse), f"Expected ToolUse, but was {type(tool_use).__name__}" - assert tool_use.id == "call_a47abadDxlGKIWjvYYvGVAHa" + assert tool_use.id == "call_Dn0idyNSdmHSYpsql3EbFH9L" assert tool_use.name == "read_file" assert tool_use.parameters == {"filename": "test.txt"} assert reply_usage.total_tokens == 125 diff --git a/packages/exchange/tests/providers/test_bedrock.py b/packages/exchange/tests/providers/test_bedrock.py index f8fcaa4b8..f7b68c034 100644 --- a/packages/exchange/tests/providers/test_bedrock.py +++ b/packages/exchange/tests/providers/test_bedrock.py @@ -15,9 +15,9 @@ @pytest.mark.parametrize( "env_var_name", [ - ("AWS_ACCESS_KEY_ID"), - ("AWS_SECRET_ACCESS_KEY"), - ("AWS_SESSION_TOKEN"), + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", ], ) def test_from_env_throw_error_when_missing_env_var(env_var_name): diff --git a/packages/exchange/tests/providers/test_databricks.py b/packages/exchange/tests/providers/test_databricks.py index 4b6793abc..cd01335a7 100644 --- a/packages/exchange/tests/providers/test_databricks.py +++ b/packages/exchange/tests/providers/test_databricks.py @@ -10,8 +10,8 @@ @pytest.mark.parametrize( "env_var_name", [ - ("DATABRICKS_HOST"), - ("DATABRICKS_TOKEN"), + "DATABRICKS_HOST", + "DATABRICKS_TOKEN", ], ) def test_from_env_throw_error_when_missing_env_var(env_var_name): @@ -61,7 +61,7 @@ def test_databricks_completion(mock_error, mock_warning, mock_sleep, mock_post, model=model, system=system, messages=messages, tools=tools ) - assert reply_message.content == [Text(text="Hello!")] + assert reply_message.content == [Text("Hello!")] assert reply_usage.total_tokens == 35 assert mock_post.call_count == 1 mock_post.assert_called_once_with( diff --git a/packages/exchange/tests/providers/test_google.py b/packages/exchange/tests/providers/test_google.py index 76ae4c8d7..a8db06475 100644 --- a/packages/exchange/tests/providers/test_google.py +++ b/packages/exchange/tests/providers/test_google.py @@ -1,13 +1,15 @@ import os from unittest.mock import patch -import httpx import pytest from exchange import Message, Text from exchange.content import ToolResult, ToolUse from exchange.providers.base import MissingProviderEnvVariableError from exchange.providers.google import GoogleProvider from exchange.tool import Tool +from .conftest import complete, tools + +GOOGLE_MODEL = os.getenv("GOOGLE_MODEL", "gemini-1.5-flash") def example_fn(param: str) -> None: @@ -30,12 +32,6 @@ def test_from_env_throw_error_when_missing_api_key(): assert "https://ai.google.dev/gemini-api/docs/api-key" in context.value.message -@pytest.fixture -@patch.dict(os.environ, {"GOOGLE_API_KEY": "test_api_key"}) -def google_provider(): - return GoogleProvider.from_env() - - def test_google_response_to_text_message() -> None: response = {"candidates": [{"content": {"parts": [{"text": "Hello from Gemini!"}], "role": "model"}}]} message = GoogleProvider.google_response_to_message(response) @@ -87,7 +83,7 @@ def test_message_text_to_google_spec() -> None: def test_messages_to_google_spec() -> None: messages = [ - Message(role="user", content=[Text(text="Hello, Gemini")]), + Message(role="user", content=[Text("Hello, Gemini")]), Message( role="assistant", content=[ToolUse(id="1", name="example_fn", parameters={"param": "value"})], @@ -105,54 +101,40 @@ def test_messages_to_google_spec() -> None: assert actual_spec == expected_spec -@patch("httpx.Client.post") -@patch("logging.warning") -@patch("logging.error") -def test_google_completion(mock_error, mock_warning, mock_post, google_provider): - mock_response = { - "candidates": [{"content": {"parts": [{"text": "Hello from Gemini!"}], "role": "model"}}], - "usageMetadata": {"promptTokenCount": 3, "candidatesTokenCount": 10, "totalTokenCount": 13}, - } +@pytest.mark.vcr() +def test_google_complete(default_google_env): + reply_message, reply_usage = complete(GoogleProvider, GOOGLE_MODEL) - # First attempts fail with status code 429, 2nd succeeds - def create_response(status_code, json_data=None): - response = httpx.Response(status_code) - response._content = httpx._content.json_dumps(json_data or {}).encode() - response._request = httpx.Request("POST", "https://generativelanguage.googleapis.com/v1beta/") - return response + assert reply_message.content == [Text("Hello! 👋 What can I do for you today? 😊 \n")] + assert reply_usage.total_tokens == 21 - mock_post.side_effect = [ - create_response(429), # 1st attempt - create_response(200, mock_response), # Final success - ] - model = "gemini-1.5-flash" - system = "You are a helpful assistant." - messages = [Message.user("Hello, Gemini")] +@pytest.mark.integration +def test_google_complete_integration(): + reply = complete(GoogleProvider, GOOGLE_MODEL) - reply_message, reply_usage = google_provider.complete(model=model, system=system, messages=messages) + assert reply[0].content is not None + print("Completion content from Google:", reply[0].content) - assert reply_message.content == [Text(text="Hello from Gemini!")] - assert reply_usage.total_tokens == 13 - assert mock_post.call_count == 2 - mock_post.assert_any_call( - "models/gemini-1.5-flash:generateContent", - json={ - "system_instruction": {"parts": [{"text": "You are a helpful assistant."}]}, - "contents": [{"role": "user", "parts": [{"text": "Hello, Gemini"}]}], - }, - ) +@pytest.mark.vcr() +def test_google_tools(default_google_env): + reply_message, reply_usage = tools(GoogleProvider, GOOGLE_MODEL) -@pytest.mark.integration -def test_google_integration(): - provider = GoogleProvider.from_env() - model = "gemini-1.5-flash" # updated model to a known valid model - system = "You are a helpful assistant." - messages = [Message.user("Hello, Gemini")] + tool_use = reply_message.content[0] + assert isinstance(tool_use, ToolUse), f"Expected ToolUse, but was {type(tool_use).__name__}" + assert tool_use.id == "read_file" + assert tool_use.name == "read_file" + assert tool_use.parameters == {"filename": "test.txt"} + assert reply_usage.total_tokens == 118 - # Run the completion - reply = provider.complete(model=model, system=system, messages=messages) - assert reply[0].content is not None - print("Completion content from Google:", reply[0].content) +@pytest.mark.integration +def test_google_tools_integration(): + reply = tools(GoogleProvider, GOOGLE_MODEL) + + tool_use = reply[0].content[0] + assert isinstance(tool_use, ToolUse), f"Expected ToolUse, but was {type(tool_use).__name__}" + assert tool_use.id is not None + assert tool_use.name == "read_file" + assert tool_use.parameters == {"filename": "test.txt"} diff --git a/packages/exchange/tests/providers/test_ollama.py b/packages/exchange/tests/providers/test_ollama.py index 3ce870d36..b1df70d5c 100644 --- a/packages/exchange/tests/providers/test_ollama.py +++ b/packages/exchange/tests/providers/test_ollama.py @@ -13,8 +13,8 @@ def test_ollama_complete(): reply_message, reply_usage = complete(OllamaProvider, OLLAMA_MODEL) - assert reply_message.content == [Text(text="Hello! I'm here to help. How can I assist you today? Let's chat. 😊")] - assert reply_usage.total_tokens == 33 + assert reply_message.content == [Text("Hello! I'm here to help. How can I assist you today? 😊")] + assert reply_usage.total_tokens == 29 @pytest.mark.integration @@ -31,7 +31,7 @@ def test_ollama_tools(): tool_use = reply_message.content[0] assert isinstance(tool_use, ToolUse), f"Expected ToolUse, but was {type(tool_use).__name__}" - assert tool_use.id == "call_z6fgu3z3" + assert tool_use.id == "call_h5d3s25w" assert tool_use.name == "read_file" assert tool_use.parameters == {"filename": "test.txt"} assert reply_usage.total_tokens == 133 diff --git a/packages/exchange/tests/providers/test_openai.py b/packages/exchange/tests/providers/test_openai.py index ea979abeb..db0a5261a 100644 --- a/packages/exchange/tests/providers/test_openai.py +++ b/packages/exchange/tests/providers/test_openai.py @@ -25,7 +25,7 @@ def test_from_env_throw_error_when_missing_api_key(): def test_openai_complete(default_openai_env): reply_message, reply_usage = complete(OpenAiProvider, OPENAI_MODEL) - assert reply_message.content == [Text(text="Hello! How can I assist you today?")] + assert reply_message.content == [Text("Hello! How can I assist you today?")] assert reply_usage.total_tokens == 27 @@ -43,7 +43,7 @@ def test_openai_tools(default_openai_env): tool_use = reply_message.content[0] assert isinstance(tool_use, ToolUse), f"Expected ToolUse, but was {type(tool_use).__name__}" - assert tool_use.id == "call_xXYlw4A7Ud1qtCopuK5gEJrP" + assert tool_use.id == "call_Z43oz2RtLmNHw9xvFgxA1SC5" assert tool_use.name == "read_file" assert tool_use.parameters == {"filename": "test.txt"} assert reply_usage.total_tokens == 122 @@ -64,7 +64,7 @@ def test_openai_tools_integration(): def test_openai_vision(default_openai_env): reply_message, reply_usage = vision(OpenAiProvider, OPENAI_MODEL) - assert reply_message.content == [Text(text='The first entry in the menu says "Ask Goose."')] + assert reply_message.content == [Text('The first entry in the menu says "Ask Goose."')] assert reply_usage.total_tokens == 14241 diff --git a/packages/exchange/tests/providers/test_provider_utils.py b/packages/exchange/tests/providers/test_provider_utils.py index 5ad0135ea..2a6ab729b 100644 --- a/packages/exchange/tests/providers/test_provider_utils.py +++ b/packages/exchange/tests/providers/test_provider_utils.py @@ -107,9 +107,9 @@ def test_messages_to_openai_spec() -> None: Message(role="user", content=[Text("How are you?")]), Message( role="assistant", - content=[ToolUse(id=1, name="tool1", parameters={"param1": "value1"})], + content=[ToolUse(id="1", name="tool1", parameters={"param1": "value1"})], ), - Message(role="user", content=[ToolResult(tool_use_id=1, output="Result")]), + Message(role="user", content=[ToolResult(tool_use_id="1", output="Result")]), ] spec = messages_to_openai_spec(messages) @@ -121,7 +121,7 @@ def test_messages_to_openai_spec() -> None: "role": "assistant", "tool_calls": [ { - "id": 1, + "id": "1", "type": "function", "function": { "name": "tool1", @@ -133,7 +133,7 @@ def test_messages_to_openai_spec() -> None: { "role": "tool", "content": "Result", - "tool_call_id": 1, + "tool_call_id": "1", }, ] @@ -216,7 +216,7 @@ def test_openai_response_to_message_valid_tooluse() -> None: expect = asdict( Message( role="assistant", - content=[ToolUse(id=1, name="example_fn", parameters={"param": "value"})], + content=[ToolUse(id="1", name="example_fn", parameters={"param": "value"})], ) ) actual.pop("id") diff --git a/packages/exchange/tests/test_exchange.py b/packages/exchange/tests/test_exchange.py index f01ef4694..34937630c 100644 --- a/packages/exchange/tests/test_exchange.py +++ b/packages/exchange/tests/test_exchange.py @@ -1,5 +1,3 @@ -from typing import List, Tuple - import pytest from exchange.checkpoint import Checkpoint, CheckpointData @@ -29,12 +27,12 @@ def no_overlapping_checkpoints(exchange: Exchange) -> bool: return True -def checkpoint_to_index_pairs(checkpoints: List[Checkpoint]) -> List[Tuple[int, int]]: +def checkpoint_to_index_pairs(checkpoints: list[Checkpoint]) -> list[tuple[int, int]]: return [(checkpoint.start_index, checkpoint.end_index) for checkpoint in checkpoints] class MockProvider(Provider): - def __init__(self, sequence: List[Message], usage_dicts: List[dict]): + def __init__(self, sequence: list[Message], usage_dicts: list[dict]): # We'll use init to provide a preplanned reply sequence self.sequence = sequence self.call_count = 0 @@ -56,11 +54,18 @@ def get_usage(data: dict) -> Usage: total_tokens=total_tokens, ) - def complete(self, model: str, system: str, messages: List[Message], tools: List[Tool]) -> Message: + def complete( + self, + model: str, + system: str, + messages: list[Message], + tools: tuple[Tool, ...], + **kwargs: dict[str, any], + ) -> tuple[Message, Usage]: output = self.sequence[self.call_count] usage = self.get_usage(self.usage_dicts[self.call_count]) self.call_count += 1 - return (output, usage) + return output, usage def test_reply_with_unsupported_tool(): @@ -116,7 +121,7 @@ def test_invalid_tool_parameters(): ), model="gpt-4o-2024-05-13", system="You are a helpful assistant.", - tools=[Tool.from_function(dummy_tool)], + tools=(Tool.from_function(dummy_tool),), moderator=PassiveModerator(), ) @@ -154,7 +159,7 @@ def test_max_tool_use_when_limit_reached(): ), model="gpt-4o-2024-05-13", system="You are a helpful assistant.", - tools=[Tool.from_function(dummy_tool)], + tools=(Tool.from_function(dummy_tool),), moderator=PassiveModerator(), ) @@ -195,7 +200,7 @@ def long_output_tool_char() -> str: ), model="gpt-4o-2024-05-13", system="You are a helpful assistant.", - tools=[Tool.from_function(long_output_tool_char)], + tools=(Tool.from_function(long_output_tool_char),), moderator=PassiveModerator(), ) @@ -236,7 +241,7 @@ def long_output_tool_token() -> str: ), model="gpt-4o-2024-05-13", system="You are a helpful assistant.", - tools=[Tool.from_function(long_output_tool_token)], + tools=(Tool.from_function(long_output_tool_token),), moderator=PassiveModerator(), ) @@ -301,7 +306,7 @@ def resumed_exchange() -> Exchange: ex = Exchange( provider=provider, messages=messages, - tools=[], + tools=(), model="gpt-4o-2024-05-13", system="You are a helpful assistant.", checkpoint_data=CheckpointData(), @@ -399,7 +404,7 @@ def test_pop_first_message_no_messages(): provider=MockProvider(sequence=[], usage_dicts=[]), model="gpt-4o-2024-05-13", system="You are a helpful assistant.", - tools=[Tool.from_function(dummy_tool)], + tools=(Tool.from_function(dummy_tool),), moderator=PassiveModerator(), ) @@ -741,7 +746,7 @@ def test_rewind_with_tool_usage(): ), model="gpt-4o-2024-05-13", system="You are a helpful assistant.", - tools=[Tool.from_function(dummy_tool)], + tools=(Tool.from_function(dummy_tool),), moderator=PassiveModerator(), ) ex.add(Message(role="user", content=[Text(text="test")])) diff --git a/packages/exchange/tests/test_exchange_frozen.py b/packages/exchange/tests/test_exchange_frozen.py index a3095b3a3..9d227afab 100644 --- a/packages/exchange/tests/test_exchange_frozen.py +++ b/packages/exchange/tests/test_exchange_frozen.py @@ -9,7 +9,7 @@ class MockProvider(Provider): - def complete(self, model, system, messages, tools=None): + def complete(self, model, system, messages, tools, **kwargs): return Message(role="assistant", content=[Text(text="This is a mock response.")]), Usage.from_dict( {"total_tokens": 35} ) diff --git a/packages/exchange/tests/test_integration_vision.py b/packages/exchange/tests/test_integration_vision.py index 20f165ade..6adf3f041 100644 --- a/packages/exchange/tests/test_integration_vision.py +++ b/packages/exchange/tests/test_integration_vision.py @@ -9,6 +9,7 @@ cases = [ (get_provider("openai"), os.getenv("OPENAI_MODEL", "gpt-4o-mini")), + (get_provider("google"), os.getenv("GOOGLE_MODEL", "gemini-1.5-flash")), ] diff --git a/packages/exchange/tests/test_langfuse_wrapper.py b/packages/exchange/tests/test_langfuse_wrapper.py new file mode 100644 index 000000000..321850978 --- /dev/null +++ b/packages/exchange/tests/test_langfuse_wrapper.py @@ -0,0 +1,46 @@ +import pytest +from unittest.mock import patch, MagicMock +from exchange.langfuse_wrapper import observe_wrapper + + +@pytest.fixture +def mock_langfuse_context(): + with patch("exchange.langfuse_wrapper.langfuse_context") as mock: + yield mock + + +@patch("exchange.langfuse_wrapper.HAS_LANGFUSE_CREDENTIALS", True) +def test_function_is_wrapped(mock_langfuse_context): + mock_observe = MagicMock(side_effect=lambda *args, **kwargs: lambda fn: fn) + mock_langfuse_context.observe = mock_observe + + def original_function(x: int, y: int) -> int: + return x + y + + # test function before we decorate it with + # @observe_wrapper("arg1", kwarg1="kwarg1") + assert not hasattr(original_function, "__wrapped__") + + # ensure we args get passed along (e.g. @observe(capture_input=False, capture_output=False)) + decorated_function = observe_wrapper("arg1", kwarg1="kwarg1")(original_function) + assert hasattr(decorated_function, "__wrapped__") + assert decorated_function.__wrapped__ is original_function, "Function is not properly wrapped" + + assert decorated_function(2, 3) == 5 + mock_observe.assert_called_once() + mock_observe.assert_called_with("arg1", kwarg1="kwarg1") + + +@patch("exchange.langfuse_wrapper.HAS_LANGFUSE_CREDENTIALS", False) +def test_function_is_not_wrapped(mock_langfuse_context): + mock_observe = MagicMock(return_value=lambda f: f) + mock_langfuse_context.observe = mock_observe + + @observe_wrapper("arg1", kwarg1="kwarg1") + def hello() -> str: + return "Hello" + + assert not hasattr(hello, "__wrapped__") + assert hello() == "Hello" + + mock_observe.assert_not_called() diff --git a/packages/exchange/tests/test_summarizer.py b/packages/exchange/tests/test_summarizer.py index fa7281920..7920fe317 100644 --- a/packages/exchange/tests/test_summarizer.py +++ b/packages/exchange/tests/test_summarizer.py @@ -3,11 +3,11 @@ from exchange.content import ToolResult, ToolUse from exchange.moderators.passive import PassiveModerator from exchange.moderators.summarizer import ContextSummarizer -from exchange.providers import Usage +from exchange.providers import Usage, Provider -class MockProvider: - def complete(self, model, system, messages, tools): +class MockProvider(Provider): + def complete(self, model, system, messages, tools, **kwargs): assistant_message_text = "Summarized content here." output_tokens = len(assistant_message_text) total_input_tokens = sum(len(msg.text) for msg in messages) @@ -138,14 +138,14 @@ def test_context_summarizer_rewrite(exchange_instance: Exchange, summarizer_inst ] -class AnotherMockProvider: +class AnotherMockProvider(Provider): def __init__(self): self.sequence = MESSAGE_SEQUENCE self.current_index = 1 self.summarize_next = False self.summarized_count = 0 - def complete(self, model, system, messages, tools): + def complete(self, model, system, messages, tools, **kwargs): system_prompt_tokens = 100 input_token_count = system_prompt_tokens diff --git a/packages/exchange/tests/test_truncate.py b/packages/exchange/tests/test_truncate.py index 3875303e7..eeb993ff1 100644 --- a/packages/exchange/tests/test_truncate.py +++ b/packages/exchange/tests/test_truncate.py @@ -73,7 +73,7 @@ def __init__(self): self.summarize_next = False self.summarized_count = 0 - def complete(self, model, system, messages, tools): + def complete(self, model, system, messages, tools, **kwargs): input_token_count = SYSTEM_PROMPT_TOKENS message = self.sequence[self.current_index] diff --git a/pyproject.toml b/pyproject.toml index 07a019266..2971b64a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "goose-ai" description = "a programming agent that runs on your machine" -version = "0.9.3" +version = "0.9.5" readme = "README.md" requires-python = ">=3.10" dependencies = [ @@ -12,6 +12,7 @@ dependencies = [ "click>=8.1.7", "prompt-toolkit>=3.0.47", "keyring>=25.4.1", + "langfuse>=2.38.2", ] author = [{ name = "Block", email = "ai-oss-tools@block.xyz" }] packages = [{ include = "goose", from = "src" }] @@ -28,6 +29,7 @@ github = "goose.toolkit.github:Github" jira = "goose.toolkit.jira:Jira" screen = "goose.toolkit.screen:Screen" language_server = "goose.toolkit.language_server:LanguageServerCoordinator" +reasoner = "goose.toolkit.reasoner:Reasoner" repo_context = "goose.toolkit.repo_context.repo_context:RepoContext" [project.entry-points."goose.profile"] diff --git a/scripts/langfuse-docker-compose.yaml b/scripts/langfuse-docker-compose.yaml new file mode 100644 index 000000000..153932120 --- /dev/null +++ b/scripts/langfuse-docker-compose.yaml @@ -0,0 +1,46 @@ +services: + langfuse-server: + image: langfuse/langfuse:2 + depends_on: + db: + condition: service_healthy + ports: + - "3000:3000" + environment: + - DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres + - NEXTAUTH_SECRET=mysecret + - SALT=mysalt + - ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32` + - NEXTAUTH_URL=http://localhost:3000 + - TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true} + - LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false} + - LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-} + - LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-} + - LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-} + - LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-} + - LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-} + - LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-} + - LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-} + - LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-} + - LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-} + + db: + image: postgres + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 3s + timeout: 3s + retries: 10 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=postgres + ports: + - 5432:5432 + volumes: + - database_data:/var/lib/postgresql/data + +volumes: + database_data: + driver: local diff --git a/scripts/setup_langfuse.sh b/scripts/setup_langfuse.sh new file mode 100755 index 000000000..480747959 --- /dev/null +++ b/scripts/setup_langfuse.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# setup_langfuse.sh +# +# This script sets up and runs Langfuse locally for development and testing purposes. +# +# Key functionalities: +# 1. Downloads the latest docker-compose.yaml from the Langfuse repository +# 2. Starts Langfuse using Docker Compose with default initialization variables +# 3. Waits for the service to be available +# 4. Launches a browser to open the local Langfuse UI +# 5. Prints login credentials from the environment file +# +# Usage: +# ./setup_langfuse.sh +# +# Requirements: +# - Docker +# - curl +# - A .env.langfuse.local file in the env directory +# +# Note: This script is intended for local development use only. + +set -e + +SCRIPT_DIR=$(realpath "$(dirname "${BASH_SOURCE[0]}")") +LANGFUSE_DOCKER_COMPOSE_URL="https://raw.githubusercontent.com/langfuse/langfuse/main/docker-compose.yml" +LANGFUSE_DOCKER_COMPOSE_FILE="langfuse-docker-compose.yaml" +LANGFUSE_ENV_FILE="$SCRIPT_DIR/../packages/exchange/.env.langfuse.local" + +check_dependencies() { + local dependencies=("curl" "docker") + local missing_dependencies=() + + for cmd in "${dependencies[@]}"; do + if ! command -v "$cmd" &> /dev/null; then + missing_dependencies+=("$cmd") + fi + done + + if [ ${#missing_dependencies[@]} -ne 0 ]; then + echo "Missing dependencies: ${missing_dependencies[*]}" + exit 1 + fi +} + +download_docker_compose() { + if ! curl --fail --location --output "$SCRIPT_DIR/langfuse-docker-compose.yaml" "$LANGFUSE_DOCKER_COMPOSE_URL"; then + echo "Failed to download docker-compose file from $LANGFUSE_DOCKER_COMPOSE_URL" + exit 1 + fi +} + +start_docker_compose() { + docker compose --env-file "$LANGFUSE_ENV_FILE" -f "$LANGFUSE_DOCKER_COMPOSE_FILE" up --detach +} + +wait_for_service() { + echo "Waiting for Langfuse to start..." + local retries=10 + local count=0 + until curl --silent http://localhost:3000 > /dev/null; do + ((count++)) + if [ "$count" -ge "$retries" ]; then + echo "Max retries reached. Langfuse did not start in time." + exit 1 + fi + sleep 1 + done + echo "Langfuse is now available!" +} + +launch_browser() { + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + xdg-open "http://localhost:3000" + elif [[ "$OSTYPE" == "darwin"* ]]; then + open "http://localhost:3000" + else + echo "Please open http://localhost:3000 to view Langfuse traces." + fi +} + +print_login_variables() { + if [ -f "$LANGFUSE_ENV_FILE" ]; then + echo "If not already logged in use the following credentials to log in:" + grep -E "LANGFUSE_INIT_USER_EMAIL|LANGFUSE_INIT_USER_PASSWORD" "$LANGFUSE_ENV_FILE" + else + echo "Langfuse environment file with local credentials not found." + fi +} + +check_dependencies +pushd "$SCRIPT_DIR" > /dev/null +download_docker_compose +start_docker_compose +wait_for_service +print_login_variables +launch_browser +popd > /dev/null diff --git a/src/goose/cli/config.py b/src/goose/cli/config.py index 7bede0be5..9b613ee1c 100644 --- a/src/goose/cli/config.py +++ b/src/goose/cli/config.py @@ -1,6 +1,6 @@ from functools import cache from pathlib import Path -from typing import Callable, Dict, Mapping, Optional, Tuple +from typing import Mapping, Optional from rich import print from rich.panel import Panel @@ -20,7 +20,7 @@ @cache -def default_profiles() -> Mapping[str, Callable]: +def default_profiles() -> Mapping[str, callable]: return load_plugins(group="goose.profile") @@ -29,7 +29,7 @@ def session_path(name: str) -> Path: return SESSIONS_PATH.joinpath(f"{name}{SESSION_FILE_SUFFIX}") -def write_config(profiles: Dict[str, Profile]) -> None: +def write_config(profiles: dict[str, Profile]) -> None: """Overwrite the config with the passed profiles""" PROFILES_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) converted = {name: profile.to_dict() for name, profile in profiles.items()} @@ -38,7 +38,7 @@ def write_config(profiles: Dict[str, Profile]) -> None: yaml.dump(converted, f) -def ensure_config(name: Optional[str]) -> Tuple[str, Profile]: +def ensure_config(name: Optional[str]) -> tuple[str, Profile]: """Ensure that the config exists and has the default section""" # TODO we should copy a templated default config in to better document # but this is complicated a bit by autodetecting the provider @@ -70,7 +70,7 @@ def ensure_config(name: Optional[str]) -> Tuple[str, Profile]: return (name, default_profile) -def read_config() -> Dict[str, Profile]: +def read_config() -> dict[str, Profile]: """Return config from the configuration file and validates its contents""" yaml = YAML() @@ -80,7 +80,7 @@ def read_config() -> Dict[str, Profile]: return {name: Profile(**profile) for name, profile in data.items()} -def default_model_configuration() -> Tuple[str, str, str]: +def default_model_configuration() -> tuple[str, str, str]: providers = load_plugins(group="exchange.provider") for provider, cls in providers.items(): try: @@ -102,6 +102,7 @@ def default_model_configuration() -> Tuple[str, str, str]: "databricks-meta-llama-3-1-70b-instruct", "databricks-meta-llama-3-1-70b-instruct", ), + "google": ("gemini-1.5-flash", "gemini-1.5-flash"), } processor, accelerator = recommended.get(provider, ("gpt-4o", "gpt-4o-mini")) return provider, processor, accelerator diff --git a/src/goose/cli/main.py b/src/goose/cli/main.py index 7d1359889..a7f484d90 100644 --- a/src/goose/cli/main.py +++ b/src/goose/cli/main.py @@ -14,6 +14,9 @@ from goose.utils.autocomplete import SUPPORTED_SHELLS, setup_autocomplete from goose.utils.session_file import list_sorted_session_files +LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +LOG_CHOICE = click.Choice(LOG_LEVELS) + @click.group() def goose_cli() -> None: @@ -136,7 +139,10 @@ def get_session_files() -> dict[str, Path]: @click.option("--profile") @click.option("--plan", type=click.Path(exists=True)) @click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="INFO") -def session_start(name: Optional[str], profile: str, log_level: str, plan: Optional[str] = None) -> None: +@click.option("--tracing", is_flag=True, required=False) +def session_start( + name: Optional[str], profile: str, log_level: str, plan: Optional[str] = None, tracing: bool = False +) -> None: """Start a new goose session""" if plan: yaml = YAML() @@ -144,8 +150,12 @@ def session_start(name: Optional[str], profile: str, log_level: str, plan: Optio _plan = yaml.load(f) else: _plan = None - session = Session(name=name, profile=profile, plan=_plan, log_level=log_level) - session.run() + + try: + session = Session(name=name, profile=profile, plan=_plan, log_level=log_level, tracing=tracing) + session.run() + except RuntimeError as e: + print(f"[red]Error: {e}") def parse_args(ctx: click.Context, param: click.Parameter, value: str) -> dict[str, str]: @@ -161,7 +171,7 @@ def parse_args(ctx: click.Context, param: click.Parameter, value: str) -> dict[s @session.command(name="planned") @click.option("--plan", type=click.Path(exists=True)) -@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="INFO") +@click.option("--log-level", type=LOG_CHOICE, default="INFO") @click.option("-a", "--args", callback=parse_args, help="Args in the format arg1:value1,arg2:value2") def session_planned(plan: str, log_level: str, args: Optional[dict[str, str]]) -> None: plan_templated = render_template(Path(plan), context=args) @@ -173,7 +183,7 @@ def session_planned(plan: str, log_level: str, args: Optional[dict[str, str]]) - @session.command(name="resume") @click.argument("name", required=False, shell_complete=autocomplete_session_files) @click.option("--profile") -@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="INFO") +@click.option("--log-level", type=LOG_CHOICE, default="INFO") def session_resume(name: Optional[str], profile: str, log_level: str) -> None: """Resume an existing goose session""" session_files = get_session_files() @@ -190,14 +200,15 @@ def session_resume(name: Optional[str], profile: str, log_level: str) -> None: else: print(f"Creating new session: {name}") session = Session(name=name, profile=profile, log_level=log_level) - session.run() + session.run(new_session=False) @goose_cli.command(name="run") @click.argument("message_file", required=False, type=click.Path(exists=True)) @click.option("--profile") -@click.option("--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), default="INFO") -def run(message_file: Optional[str], profile: str, log_level: str) -> None: +@click.option("--log-level", type=LOG_CHOICE, default="INFO") +@click.option("--resume-session", is_flag=True, help="Resume the last session if available") +def run(message_file: Optional[str], profile: str, log_level: str, resume_session: bool = False) -> None: """Run a single-pass session with a message from a markdown input file""" if message_file: with open(message_file, "r") as f: @@ -205,7 +216,13 @@ def run(message_file: Optional[str], profile: str, log_level: str) -> None: else: initial_message = click.get_text_stream("stdin").read() - session = Session(profile=profile, log_level=log_level) + if resume_session: + session_files = get_session_files() + if session_files: + name = list(session_files.keys())[0] + session = Session(name=name, profile=profile, log_level=log_level) + else: + session = Session(profile=profile, log_level=log_level) session.single_pass(initial_message=initial_message) diff --git a/src/goose/cli/prompt/completer.py b/src/goose/cli/prompt/completer.py index 6739d1530..fb453fd38 100644 --- a/src/goose/cli/prompt/completer.py +++ b/src/goose/cli/prompt/completer.py @@ -1,5 +1,4 @@ import re -from typing import List from prompt_toolkit.completion import CompleteEvent, Completer, Completion from prompt_toolkit.document import Document @@ -8,10 +7,10 @@ class GoosePromptCompleter(Completer): - def __init__(self, commands: List[Command]) -> None: + def __init__(self, commands: list[Command]) -> None: self.commands = commands - def get_command_completions(self, document: Document) -> List[Completion]: + def get_command_completions(self, document: Document) -> list[Completion]: all_completions = [] for command_name, command_instance in self.commands.items(): pattern = rf"(? List[Completion]: all_completions.extend(completions) return all_completions - def get_command_name_completions(self, document: Document) -> List[Completion]: + def get_command_name_completions(self, document: Document) -> list[Completion]: pattern = r"(? List[Completion]: completions.append(Completion(command_name, start_position=-len(query), display=command_name)) return completions - def get_completions(self, document: Document, _: CompleteEvent) -> List[Completion]: + def get_completions(self, document: Document, _: CompleteEvent) -> list[Completion]: command_completions = self.get_command_completions(document) command_name_completions = self.get_command_name_completions(document) return command_name_completions + command_completions diff --git a/src/goose/cli/prompt/lexer.py b/src/goose/cli/prompt/lexer.py index 0e2bb0c91..e00fd207a 100644 --- a/src/goose/cli/prompt/lexer.py +++ b/src/goose/cli/prompt/lexer.py @@ -1,5 +1,5 @@ import re -from typing import Callable, List, Tuple +from typing import Callable from prompt_toolkit.document import Document from prompt_toolkit.lexers import Lexer @@ -27,7 +27,7 @@ def value_for_command(command_string: str) -> re.Pattern[str]: class PromptLexer(Lexer): - def __init__(self, command_names: List[str]) -> None: + def __init__(self, command_names: list[str]) -> None: self.patterns = [] for command_name in command_names: self.patterns.append((completion_for_command(command_name), "class:command")) @@ -35,7 +35,7 @@ def __init__(self, command_names: List[str]) -> None: self.patterns.append((command_itself(command_name), "class:command")) def lex_document(self, document: Document) -> Callable[[int], list]: - def get_line_tokens(line_number: int) -> Tuple[str, str]: + def get_line_tokens(line_number: int) -> tuple[str, str]: line = document.lines[line_number] tokens = [] diff --git a/src/goose/cli/prompt/overwrite_session_prompt.py b/src/goose/cli/prompt/overwrite_session_prompt.py new file mode 100644 index 000000000..64bbeed61 --- /dev/null +++ b/src/goose/cli/prompt/overwrite_session_prompt.py @@ -0,0 +1,30 @@ +from rich.prompt import Prompt + + +class OverwriteSessionPrompt(Prompt): + def __init__(self, *args: tuple[any], **kwargs: dict[str, any]) -> None: + super().__init__(*args, **kwargs) + self.choices = { + "yes": "Overwrite the existing session", + "no": "Pick a new session name", + "resume": "Resume the existing session", + } + self.default = "resume" + + def check_choice(self, choice: str) -> bool: + normalized_choice = choice.lower() + for key in self.choices: + is_key = normalized_choice == key + is_first_letter = normalized_choice and normalized_choice[0] == key[0] + if is_key or is_first_letter: + return True + return False + + def pre_prompt(self) -> str: + print("Would you like to overwrite it?") + print() + for key, value in self.choices.items(): + first_letter, remaining = key[0], key[1:] + rendered_key = rf"[{first_letter}]{remaining}" + print(f" {rendered_key:10} {value}") + print() diff --git a/src/goose/cli/session.py b/src/goose/cli/session.py index 83332f113..b8182f58c 100644 --- a/src/goose/cli/session.py +++ b/src/goose/cli/session.py @@ -1,24 +1,29 @@ from contextlib import nullcontext +import logging import traceback from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Optional -from exchange import Message, ToolResult, ToolUse, Text +from langfuse.decorators import langfuse_context +from exchange import Message, Text, ToolResult, ToolUse +from exchange.langfuse_wrapper import observe_wrapper, auth_check from rich import print from rich.markdown import Markdown from rich.panel import Panel +from rich.prompt import Prompt from rich.status import Status -from goose.cli.config import ensure_config, session_path, LOG_PATH from goose._logger import get_logger, setup_logging +from goose.cli.config import LOG_PATH, ensure_config, session_path from goose.cli.prompt.goose_prompt_session import GoosePromptSession +from goose.cli.prompt.overwrite_session_prompt import OverwriteSessionPrompt from goose.cli.session_notifier import SessionNotifier from goose.profile import Profile from goose.toolkit.language_server import LanguageServerCoordinator from goose.utils import droid, load_plugins from goose.utils._cost_calculator import get_total_cost_message from goose.utils._create_exchange import create_exchange -from goose.utils.session_file import read_or_create_file, save_latest_session +from goose.utils.session_file import is_empty_session, is_existing_session, read_or_create_file, save_latest_session RESUME_MESSAGE = "I see we were interrupted. How can I help you?" @@ -62,7 +67,8 @@ def __init__( profile: Optional[str] = None, plan: Optional[dict] = None, log_level: Optional[str] = "INFO", - **kwargs: Dict[str, Any], + tracing: bool = False, + **kwargs: dict[str, any], ) -> None: if name is None: self.name = droid() @@ -73,6 +79,18 @@ def __init__( self.status_indicator = Status("", spinner="dots") self.notifier = SessionNotifier(self.status_indicator) self.profile = load_profile(profile) + if not tracing: + logging.getLogger("langfuse").setLevel(logging.ERROR) + else: + langfuse_auth = auth_check() + if langfuse_auth: + print("Local Langfuse initialized. View your traces at http://localhost:3000") + else: + raise RuntimeError( + "You passed --tracing, but a Langfuse object was not found in the current context. " + "Please initialize the local Langfuse server and restart Goose." + ) + langfuse_context.configure(enabled=tracing) self.exchange = create_exchange(profile=self.profile, notifier=self.notifier) setup_logging(log_file_directory=LOG_PATH, log_level=log_level) @@ -84,7 +102,7 @@ def __init__( self.prompt_session = GoosePromptSession() - def _get_initial_messages(self) -> List[Message]: + def _get_initial_messages(self) -> list[Message]: messages = self.load_session() if messages and messages[-1].role == "user": @@ -155,13 +173,19 @@ def single_pass(self, initial_message: str) -> None: print(f"[dim]ended run | name:[cyan]{self.name}[/] profile:[cyan]{profile}[/]") print(f"[dim]to resume: [magenta]goose session resume {self.name} --profile {profile}[/][/]") - def run(self) -> None: + def run(self, new_session: bool = True) -> None: """ Runs the main loop to handle user inputs and responses. Continues until an empty string is returned from the prompt. + + Args: + new_session (bool): True when starting a new session, False when resuming. """ - print(f"[dim]starting session | name:[cyan]{self.name}[/] profile:[cyan]{self.profile_name or 'default'}[/]") - print(f"[dim]saving to {self.session_file_path}") + if is_existing_session(self.session_file_path) and new_session: + self._prompt_overwrite_session() + + profile_name = self.profile_name or "default" + print(f"[dim]starting session | name: [cyan]{self.name}[/cyan] profile: [cyan]{profile_name}[/cyan][/dim]") print() message = self.process_first_message() with self.setup_language_server()() as _: @@ -191,8 +215,10 @@ def run(self) -> None: user_input = self.prompt_session.get_user_input() message = Message.user(text=user_input.text) if user_input.to_continue() else None + self._remove_empty_session() self._log_cost() + @observe_wrapper() def reply(self) -> None: """Reply to the last user message, calling tools as needed""" self.status_indicator.update("responding") @@ -247,12 +273,53 @@ def interrupt_reply(self) -> None: def session_file_path(self) -> Path: return session_path(self.name) - def load_session(self) -> List[Message]: + def load_session(self) -> list[Message]: return read_or_create_file(self.session_file_path) def _log_cost(self) -> None: get_logger().info(get_total_cost_message(self.exchange.get_token_usage())) - print(f"[dim]you can view the cost and token usage in the log directory {LOG_PATH}") + print(f"[dim]you can view the cost and token usage in the log directory {LOG_PATH}[/]") + + def _prompt_overwrite_session(self) -> None: + print(f"[yellow]Session already exists at {self.session_file_path}.[/]") + + choice = OverwriteSessionPrompt.ask("Enter your choice", show_choices=False) + match choice: + case "y" | "yes": + print("Overwriting existing session") + + case "n" | "no": + while True: + new_session_name = Prompt.ask("Enter a new session name") + if not is_existing_session(session_path(new_session_name)): + self.name = new_session_name + break + print(f"[yellow]Session '{new_session_name}' already exists[/]") + + case "r" | "resume": + self.exchange.messages.extend(self.load_session()) + + def _remove_empty_session(self) -> bool: + """ + Removes the session file only when it's empty. + + Note: This is because a session file is created at the start of the run + loop. When a user aborts before their first message empty session files + will be created, causing confusion when resuming sessions (which + depends on most recent mtime and is non-empty). + + Returns: + bool: True if the session file was removed, False otherwise. + """ + logger = get_logger() + try: + if is_empty_session(self.session_file_path): + logger.debug(f"deleting empty session file: {self.session_file_path}") + self.session_file_path.unlink() + return True + except Exception as e: + logger.error(f"error deleting empty session file: {e}") + return False if __name__ == "__main__": diff --git a/src/goose/command/__init__.py b/src/goose/command/__init__.py index d9fd674a4..cef47fec9 100644 --- a/src/goose/command/__init__.py +++ b/src/goose/command/__init__.py @@ -1,5 +1,4 @@ from functools import cache -from typing import Dict from goose.command.base import Command from goose.utils import load_plugins @@ -11,5 +10,5 @@ def get_command(name: str) -> type[Command]: @cache -def get_commands() -> Dict[str, type[Command]]: +def get_commands() -> dict[str, type[Command]]: return load_plugins(group="goose.command") diff --git a/src/goose/command/base.py b/src/goose/command/base.py index 5a8c346ff..081453de6 100644 --- a/src/goose/command/base.py +++ b/src/goose/command/base.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import List, Optional +from typing import Optional from prompt_toolkit.completion import Completion @@ -7,7 +7,7 @@ class Command(ABC): """A command that can be executed by the CLI.""" - def get_completions(self, query: str) -> List[Completion]: + def get_completions(self, query: str) -> list[Completion]: """ Get completions for the command. diff --git a/src/goose/command/file.py b/src/goose/command/file.py index cb8bdfd67..786785cf8 100644 --- a/src/goose/command/file.py +++ b/src/goose/command/file.py @@ -1,5 +1,4 @@ import os -from typing import List from prompt_toolkit.completion import Completion @@ -7,7 +6,7 @@ class FileCommand(Command): - def get_completions(self, query: str) -> List[Completion]: + def get_completions(self, query: str) -> list[Completion]: if query.startswith("/"): directory = os.path.dirname(query) search_term = os.path.basename(query) diff --git a/src/goose/profile.py b/src/goose/profile.py index cdc34fb85..2999a470a 100644 --- a/src/goose/profile.py +++ b/src/goose/profile.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Mapping, Type +from typing import Mapping from attrs import asdict, define, field @@ -21,10 +21,10 @@ class Profile: processor: str accelerator: str moderator: str - toolkits: List[ToolkitSpec] = field(factory=list, converter=ensure_list(ToolkitSpec)) + toolkits: list[ToolkitSpec] = field(factory=list, converter=ensure_list(ToolkitSpec)) @toolkits.validator - def check_toolkit_requirements(self, _: Type["ToolkitSpec"], toolkits: List[ToolkitSpec]) -> None: + def check_toolkit_requirements(self, _: type["ToolkitSpec"], toolkits: list[ToolkitSpec]) -> None: # checks that the list of toolkits in the profile have their requirements installed_toolkits = set([toolkit.name for toolkit in toolkits]) @@ -36,7 +36,7 @@ def check_toolkit_requirements(self, _: Type["ToolkitSpec"], toolkits: List[Tool msg = f"Toolkit {toolkit_name} requires {req} but it is not present" raise ValueError(msg) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, any]: return asdict(self) def profile_info(self) -> str: @@ -44,7 +44,7 @@ def profile_info(self) -> str: return f"provider:{self.provider}, processor:{self.processor} toolkits: {', '.join(tookit_names)}" -def default_profile(provider: str, processor: str, accelerator: str, **kwargs: Dict[str, Any]) -> Profile: +def default_profile(provider: str, processor: str, accelerator: str, **kwargs: dict[str, any]) -> Profile: """Get the default profile""" # TODO consider if the providers should have recommended models diff --git a/src/goose/toolkit/base.py b/src/goose/toolkit/base.py index 850f40263..faa102d3d 100644 --- a/src/goose/toolkit/base.py +++ b/src/goose/toolkit/base.py @@ -1,6 +1,6 @@ import inspect from abc import ABC -from typing import Callable, Mapping, Optional, Tuple, TypeVar +from typing import Mapping, Optional, TypeVar from attrs import define, field from exchange import Tool @@ -8,7 +8,7 @@ from goose.notifier import Notifier # Create a type variable that can represent any function signature -F = TypeVar("F", bound=Callable) +F = TypeVar("F", bound=callable) def tool(func: F) -> F: @@ -55,7 +55,7 @@ def system(self) -> str: """Get the addition to the system prompt for this toolkit.""" return "" - def tools(self) -> Tuple[Tool, ...]: + def tools(self) -> tuple[Tool, ...]: """Get the tools for this toolkit This default method looks for functions on the toolkit annotated diff --git a/src/goose/toolkit/developer.py b/src/goose/toolkit/developer.py index ba600d921..b48a18069 100644 --- a/src/goose/toolkit/developer.py +++ b/src/goose/toolkit/developer.py @@ -3,7 +3,6 @@ import subprocess import time from pathlib import Path -from typing import Dict, List from exchange import Message from goose.toolkit.base import Toolkit, tool @@ -35,9 +34,9 @@ class Developer(Toolkit): We also include some default shell strategies in the prompt, such as using ripgrep """ - def __init__(self, *args: object, **kwargs: Dict[str, object]) -> None: + def __init__(self, *args: object, **kwargs: dict[str, object]) -> None: super().__init__(*args, **kwargs) - self.timestamps: Dict[str, float] = {} + self.timestamps: dict[str, float] = {} def system(self) -> str: """Retrieve system configuration details for developer""" @@ -55,7 +54,7 @@ def system(self) -> str: return system_prompt @tool - def update_plan(self, tasks: List[dict]) -> List[dict]: + def update_plan(self, tasks: list[dict]) -> list[dict]: """ Update the plan by overwriting all current tasks @@ -63,7 +62,7 @@ def update_plan(self, tasks: List[dict]) -> List[dict]: shown to the user directly, you do not need to reiterate it Args: - tasks (List(dict)): The list of tasks, where each task is a dictionary + tasks (list(dict)): The list of tasks, where each task is a dictionary with a key for the task "description" and the task "status". The status MUST be one of "planned", "complete", "failed", "in-progress". diff --git a/src/goose/toolkit/prompts/reasoner.jinja b/src/goose/toolkit/prompts/reasoner.jinja new file mode 100644 index 000000000..0129eb6ba --- /dev/null +++ b/src/goose/toolkit/prompts/reasoner.jinja @@ -0,0 +1,5 @@ +It is important to use deep reasoning and thinking tools when working with code, especially solving problems or new issues. +Writing code requires deep thinking and reasoning at times, which can be used to provide ideas, solutions, and to check other solutions. +Always use your generate_code tool when writing code especially on a new problem, +and use deep_reason to check solutions or when there have been errors or solutions are not clear. +Consider these tools as expert consultants that can provide advice and code that you may use. \ No newline at end of file diff --git a/src/goose/toolkit/reasoner.py b/src/goose/toolkit/reasoner.py new file mode 100644 index 000000000..fe8f3aa23 --- /dev/null +++ b/src/goose/toolkit/reasoner.py @@ -0,0 +1,77 @@ +from exchange import Exchange, Message, Text +from exchange.content import Content +from exchange.providers import OpenAiProvider +from goose.toolkit.base import Toolkit, tool +from goose.utils.ask import ask_an_ai + + +class Reasoner(Toolkit): + """Deep thinking toolkit for reasoning through problems and solutions""" + + def message_content(self, content: Content) -> Text: + if isinstance(content, Text): + return content + else: + return Text(str(content)) + + @tool + def deep_reason(self, problem: str) -> str: + """ + Debug or reason about challenges or problems. + It will take a minute to think about it and consider solutions. + + Args: + problem (str): description of problem or errors seen. + + Returns: + response (str): A solution, which may include a suggestion or code snippet. + """ + # Create an instance of Exchange with the inlined OpenAI provider + self.notifier.status("thinking...") + provider = OpenAiProvider.from_env() + + # Create messages list + existing_messages_copy = [ + Message(role=msg.role, content=[self.message_content(content) for content in msg.content]) + for msg in self.exchange_view.processor.messages + ] + exchange = Exchange(provider=provider, model="o1-preview", messages=existing_messages_copy, system=None) + + response = ask_an_ai(input="please help reason about this: " + problem, exchange=exchange, no_history=False) + return response.content[0].text + + @tool + def generate_code(self, instructions: str) -> str: + """ + reason about and write code based on instructions given. + this will consider and reason about the instructions and come up with code to solve it. + + Args: + instructions (str): instructions of what code to write or how to modify it. + + Returns: + response (str): generated code to be tested or applied. Not it will not write directly to files so you have to take it and process it if it is suitable. + """ # noqa: E501 + # Create an instance of Exchange with the inlined OpenAI provider + provider = OpenAiProvider.from_env() + + # clone messages, converting to text for context + existing_messages_copy = [ + Message(role=msg.role, content=[self.message_content(content) for content in msg.content]) + for msg in self.exchange_view.processor.messages + ] + exchange = Exchange(provider=provider, model="o1-mini", messages=existing_messages_copy, system=None) + + self.notifier.status("generating code...") + response = ask_an_ai( + input="Please follow the instructions, " + + "and ideally return relevant code and little commentary:" + + instructions, + exchange=exchange, + no_history=False, + ) + return response.content[0].text + + def system(self) -> str: + """Retrieve instructions on how to use this reasoning and code generation tool""" + return Message.load("prompts/reasoner.jinja").text diff --git a/src/goose/toolkit/repo_context/repo_context.py b/src/goose/toolkit/repo_context/repo_context.py index 8be8794f6..b8d0f1aae 100644 --- a/src/goose/toolkit/repo_context/repo_context.py +++ b/src/goose/toolkit/repo_context/repo_context.py @@ -1,7 +1,6 @@ import os from functools import cache from subprocess import CompletedProcess, run -from typing import Dict, Tuple from exchange import Message @@ -21,7 +20,7 @@ def __init__(self, notifier: Notifier, requires: Requirements) -> None: self.repo_project_root, self.is_git_repo, self.goose_session_root = self.determine_git_proj() - def determine_git_proj(self) -> Tuple[str, bool, str]: + def determine_git_proj(self) -> tuple[str, bool, str]: """Determines the root as well as where Goose is currently running If the project is not part of a Github repo, the root of the project will be defined as the current working @@ -72,11 +71,11 @@ def is_mono_repo(self) -> bool: return self.repo_size > 2000 @tool - def summarize_current_project(self) -> Dict[str, str]: + def summarize_current_project(self) -> dict[str, str]: """Summarizes the current project based on repo root (if git repo) or current project_directory (if not) Returns: - summary (Dict[str, str]): Keys are file paths and values are the summaries + summary (dict[str, str]): Keys are file paths and values are the summaries """ self.notifier.log("Summarizing the most relevant files in the current project. This may take a while...") @@ -101,8 +100,11 @@ def summarize_current_project(self) -> Dict[str, str]: file_select_exchange = replace_prompt(exchange=file_select_exchange, prompt=system) files = goose_picks_files(root=project_directory, exchange=file_select_exchange) + # summarize the selected files using a blank exchange with no tools summary = summarize_files_concurrent( - exchange=self.exchange_view.accelerator, file_list=files, project_name=project_directory.split("/")[-1] + exchange=clear_exchange(self.exchange_view.accelerator, clear_tools=True), + file_list=files, + project_name=project_directory.split("/")[-1], ) return summary diff --git a/src/goose/toolkit/repo_context/utils.py b/src/goose/toolkit/repo_context/utils.py index dca7f04b0..e69cea936 100644 --- a/src/goose/toolkit/repo_context/utils.py +++ b/src/goose/toolkit/repo_context/utils.py @@ -2,7 +2,6 @@ import concurrent.futures import os from collections import deque -from typing import Dict, List, Tuple from exchange import Exchange @@ -26,7 +25,7 @@ def get_repo_size(repo_path: str) -> int: return get_directory_size(git_dir) / (1024**2) -def get_files_and_directories(root_dir: str) -> Dict[str, list]: +def get_files_and_directories(root_dir: str) -> dict[str, list]: """Gets file names and directory names. Checks that goose has correctly typed the file and directory names and that the files actually exist (to avoid downstream file read errors). @@ -61,7 +60,7 @@ def get_files_and_directories(root_dir: str) -> Dict[str, list]: return {"files": files, "directories": dirs} -def goose_picks_files(root: str, exchange: Exchange, max_workers: int = 4) -> List[str]: +def goose_picks_files(root: str, exchange: Exchange, max_workers: int = 4) -> list[str]: """Lets goose pick files in a BFS manner""" queue = deque([root]) @@ -80,7 +79,7 @@ def goose_picks_files(root: str, exchange: Exchange, max_workers: int = 4) -> Li return all_files -def process_directory(current_dir: str, exchange: Exchange) -> Tuple[List[str], List[str]]: +def process_directory(current_dir: str, exchange: Exchange) -> tuple[list[str], list[str]]: """Allows goose to pick files and subdirectories contained in a given directory (current_dir). Get the list of file and directory names in the current folder, then ask Goose to pick which ones to keep. diff --git a/src/goose/toolkit/summarization/summarize_project.py b/src/goose/toolkit/summarization/summarize_project.py index d910fbc47..f5e22562c 100644 --- a/src/goose/toolkit/summarization/summarize_project.py +++ b/src/goose/toolkit/summarization/summarize_project.py @@ -1,5 +1,5 @@ import os -from typing import List, Optional +from typing import Optional from goose.toolkit import Toolkit from goose.toolkit.base import tool @@ -11,7 +11,7 @@ class SummarizeProject(Toolkit): def get_project_summary( self, project_dir_path: Optional[str] = os.getcwd(), - extensions: Optional[List[str]] = None, + extensions: Optional[list[str]] = None, summary_instructions_prompt: Optional[str] = None, ) -> dict: """Generates or retrieves a project summary based on specified file extensions. @@ -19,7 +19,7 @@ def get_project_summary( Args: project_dir_path (Optional[Path]): Path to the project directory. Defaults to the current working directory if None - extensions (Optional[List[str]]): Specific file extensions to summarize. + extensions (Optional[list[str]]): Specific file extensions to summarize. summary_instructions_prompt (Optional[str]): Instructions to give to the LLM about how to summarize each file. E.g. "Summarize the file in two sentences.". The default instruction is "Please summarize this file." diff --git a/src/goose/toolkit/summarization/summarize_repo.py b/src/goose/toolkit/summarization/summarize_repo.py index 18c7da428..58765dd9a 100644 --- a/src/goose/toolkit/summarization/summarize_repo.py +++ b/src/goose/toolkit/summarization/summarize_repo.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from goose.toolkit import Toolkit from goose.toolkit.base import tool @@ -10,7 +10,7 @@ class SummarizeRepo(Toolkit): def summarize_repo( self, repo_url: str, - specified_extensions: Optional[List[str]] = None, + specified_extensions: Optional[list[str]] = None, summary_instructions_prompt: Optional[str] = None, ) -> dict: """ @@ -19,7 +19,7 @@ def summarize_repo( Args: repo_url (str): The URL of the repository to summarize. - specified_extensions (Optional[List[str]]): List of file extensions to summarize, e.g., ["tf", "md"]. If + specified_extensions (Optional[list[str]]): list of file extensions to summarize, e.g., ["tf", "md"]. If this list is empty, then all files in the repo are summarized summary_instructions_prompt (Optional[str]): Instructions to give to the LLM about how to summarize each file. E.g. "Summarize the file in two sentences.". The default instruction is "Please summarize this file." diff --git a/src/goose/toolkit/summarization/utils.py b/src/goose/toolkit/summarization/utils.py index d398713cc..96e5d363d 100644 --- a/src/goose/toolkit/summarization/utils.py +++ b/src/goose/toolkit/summarization/utils.py @@ -2,7 +2,7 @@ import subprocess from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import Optional from exchange import Exchange from exchange.providers.utils import InitialMessageTooLargeError @@ -15,7 +15,7 @@ # TODO: move git stuff -def run_git_command(command: List[str]) -> subprocess.CompletedProcess[str]: +def run_git_command(command: list[str]) -> subprocess.CompletedProcess[str]: result = subprocess.run(["git"] + command, capture_output=True, text=True, check=False) if result.returncode != 0: @@ -28,7 +28,7 @@ def clone_repo(repo_url: str, target_directory: str) -> None: run_git_command(["clone", repo_url, target_directory]) -def load_summary_file_if_exists(project_name: str) -> Optional[Dict]: +def load_summary_file_if_exists(project_name: str) -> Optional[dict]: """Checks if a summary file exists at '.goose/summaries/projectname-summary.json. Returns contents of the file if it exists, otherwise returns None @@ -36,7 +36,7 @@ def load_summary_file_if_exists(project_name: str) -> Optional[Dict]: project_name (str): name of the project or repo Returns: - Optional[Dict]: File contents, else None + Optional[dict]: File contents, else None """ summary_file_path = f"{SUMMARIES_FOLDER}/{project_name}-summary.json" if Path(summary_file_path).exists(): @@ -44,7 +44,7 @@ def load_summary_file_if_exists(project_name: str) -> Optional[Dict]: return json.load(f) -def summarize_file(filepath: str, exchange: Exchange, prompt: Optional[str] = None) -> Tuple[str, str]: +def summarize_file(filepath: str, exchange: Exchange, prompt: Optional[str] = None) -> tuple[str, str]: """Summarizes a single file Args: @@ -74,15 +74,15 @@ def summarize_file(filepath: str, exchange: Exchange, prompt: Optional[str] = No def summarize_repo( repo_url: str, exchange: Exchange, - extensions: List[str], + extensions: list[str], summary_instructions_prompt: Optional[str] = None, -) -> Dict[str, str]: +) -> dict[str, str]: """Clones (if needed) and summarizes a repo Args: repo_url (str): Repository url exchange (Exchange): Exchange for summarizing the repo. - extensions (List[str]): List of file-types to summarize. + extensions (list[str]): list of file-types to summarize. summary_instructions_prompt (Optional[str]): Optional parameter to customize summarization results. Defaults to "Please summarize this file" """ @@ -110,15 +110,15 @@ def summarize_repo( def summarize_directory( - directory: str, exchange: Exchange, extensions: List[str], summary_instructions_prompt: Optional[str] = None -) -> Dict[str, str]: + directory: str, exchange: Exchange, extensions: list[str], summary_instructions_prompt: Optional[str] = None +) -> dict[str, str]: """Summarize files in a given directory based on extensions. Will also recursively find files in subdirectories and summarize them. Args: directory (str): path to the top-level directory to summarize exchange (Exchange): Exchange to use to summarize - extensions (List[str]): List of file-type extensions to summarize (and ignore all other extensions). + extensions (list[str]): list of file-type extensions to summarize (and ignore all other extensions). summary_instructions_prompt (Optional[str]): Optional instructions to give to the exchange regarding summarization. Returns: @@ -158,19 +158,19 @@ def summarize_directory( def summarize_files_concurrent( - exchange: Exchange, file_list: List[str], project_name: str, summary_instructions_prompt: Optional[str] = None -) -> Dict[str, str]: + exchange: Exchange, file_list: list[str], project_name: str, summary_instructions_prompt: Optional[str] = None +) -> dict[str, str]: """Takes in a list of files and summarizes them. Exchange does not keep history of the summarized files. Args: exchange (Exchange): Underlying exchange - file_list (List[str]): List of paths to files to summarize + file_list (list[str]): list of paths to files to summarize project_name (str): Used to save the summary of the files to .goose/summaries/-summary.json summary_instructions_prompt (Optional[str]): Summary instructions for the LLM. Defaults to "Please summarize this file." Returns: - file_summaries (Dict[str, str]): Keys are file paths and values are the summaries returned by the Exchange + file_summaries (dict[str, str]): Keys are file paths and values are the summaries returned by the Exchange """ summary_file = load_summary_file_if_exists(project_name) if summary_file: diff --git a/src/goose/toolkit/utils.py b/src/goose/toolkit/utils.py index ad97360f2..d6f0335b1 100644 --- a/src/goose/toolkit/utils.py +++ b/src/goose/toolkit/utils.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Optional, Dict +from typing import Optional from pygments.lexers import get_lexer_for_filename from pygments.util import ClassNotFound @@ -67,7 +67,7 @@ def find_last_task_group_index(input_str: str) -> int: return last_group_start_index -def parse_plan(input_plan_str: str) -> Dict: +def parse_plan(input_plan_str: str) -> dict: last_group_start_index = find_last_task_group_index(input_plan_str) if last_group_start_index == -1: return {"kickoff_message": input_plan_str, "tasks": []} diff --git a/src/goose/utils/__init__.py b/src/goose/utils/__init__.py index 69887e7f0..d9535b187 100644 --- a/src/goose/utils/__init__.py +++ b/src/goose/utils/__init__.py @@ -1,7 +1,7 @@ import random import string from importlib.metadata import entry_points -from typing import Any, Callable, Dict, List, Type, TypeVar +from typing import TypeVar, Callable T = TypeVar("T") @@ -31,10 +31,10 @@ def load_plugins(group: str) -> dict: return plugins -def ensure(cls: Type[T]) -> Callable[[Any], T]: +def ensure(cls: type[T]) -> Callable[[any], T]: """Convert dictionary to a class instance""" - def converter(val: Any) -> T: # noqa: ANN401 + def converter(val: any) -> T: # noqa: ANN401 if isinstance(val, cls): return val elif isinstance(val, dict): @@ -47,10 +47,10 @@ def converter(val: Any) -> T: # noqa: ANN401 return converter -def ensure_list(cls: Type[T]) -> Callable[[List[Dict[str, Any]]], Type[T]]: +def ensure_list(cls: type[T]) -> Callable[[list[dict[str, any]]], type[T]]: """Convert a list of dictionaries to class instances""" - def converter(val: List[Dict[str, Any]]) -> List[T]: + def converter(val: list[dict[str, any]]) -> list[T]: output = [] for entry in val: output.append(ensure(cls)(entry)) diff --git a/src/goose/utils/file_utils.py b/src/goose/utils/file_utils.py index 370ce6565..d3f9e8e1b 100644 --- a/src/goose/utils/file_utils.py +++ b/src/goose/utils/file_utils.py @@ -2,7 +2,7 @@ import os from collections import Counter from pathlib import Path -from typing import Dict, List, Optional +from typing import Optional from goose.utils.language import Language @@ -13,7 +13,7 @@ def create_extensions_list(project_root: str, max_n: int) -> list: project_root (str): Root of the project to analyze max_n (int): The number of file extensions to return Returns: - extensions (List[str]): A list of the top N file extensions + extensions (list[str]): A list of the top N file extensions """ if max_n == 0: raise (ValueError("Number of file extensions must be greater than 0")) @@ -33,14 +33,14 @@ def create_extensions_list(project_root: str, max_n: int) -> list: return extensions -def create_language_weighting(files_in_directory: List[str]) -> Dict[str, float]: +def create_language_weighting(files_in_directory: list[str]) -> dict[str, float]: """Calculate language weighting by file size to match GitHub's methodology. Args: - files_in_directory (List[str]): Paths to files in the project directory + files_in_directory (list[str]): Paths to files in the project directory Returns: - Dict[str, float]: A dictionary with languages as keys and their percentage of the total codebase as values + dict[str, float]: A dictionary with languages as keys and their percentage of the total codebase as values """ # Initialize counters for sizes @@ -61,7 +61,7 @@ def create_language_weighting(files_in_directory: List[str]) -> Dict[str, float] return dict(sorted(language_percentages.items(), key=lambda item: item[1], reverse=True)) -def list_files_with_extension(dir_path: str, extension: Optional[str] = "") -> List[str]: +def list_files_with_extension(dir_path: str, extension: Optional[str] = "") -> list[str]: """List all files in a directory with a given extension. Set extension to '' to return all files. Args: @@ -69,7 +69,7 @@ def list_files_with_extension(dir_path: str, extension: Optional[str] = "") -> L extension (Optional[str]): extension to lookup. Defaults to '' which will return all files. Returns: - files (List[str]): List of file paths + files (list[str]): list of file paths """ # add a leading '.' to extension if needed if extension and not extension.startswith("."): @@ -79,15 +79,15 @@ def list_files_with_extension(dir_path: str, extension: Optional[str] = "") -> L return files -def create_file_list(dir_path: str, extensions: List[str]) -> List[str]: +def create_file_list(dir_path: str, extensions: list[str]) -> list[str]: """Creates a list of files with certain extensions Args: dir_path (str): Directory to list files of. Will include files recursively in sub-directories. - extensions (List[str]): List of file extensions to select for. If empty list, return all files + extensions (list[str]): list of file extensions to select for. If empty list, return all files Returns: - final_file_list (List[str]): List of file paths with specified extensions. + final_file_list (list[str]): list of file paths with specified extensions. """ # if extensions is empty list, return all files if not extensions: diff --git a/src/goose/utils/session_file.py b/src/goose/utils/session_file.py index 435186ce5..510b4aa19 100644 --- a/src/goose/utils/session_file.py +++ b/src/goose/utils/session_file.py @@ -1,20 +1,28 @@ import json import os -from pathlib import Path import tempfile -from typing import Dict, Iterator, List +from pathlib import Path +from typing import Iterator from exchange import Message from goose.cli.config import SESSION_FILE_SUFFIX -def write_to_file(file_path: Path, messages: List[Message]) -> None: +def is_existing_session(path: Path) -> bool: + return path.is_file() and path.stat().st_size > 0 + + +def is_empty_session(path: Path) -> bool: + return path.is_file() and path.stat().st_size == 0 + + +def write_to_file(file_path: Path, messages: list[Message]) -> None: with open(file_path, "w") as f: _write_messages_to_file(f, messages) -def read_or_create_file(file_path: Path) -> List[Message]: +def read_or_create_file(file_path: Path) -> list[Message]: if file_path.exists(): return read_from_file(file_path) with open(file_path, "w"): @@ -22,7 +30,7 @@ def read_or_create_file(file_path: Path) -> List[Message]: return [] -def read_from_file(file_path: Path) -> List[Message]: +def read_from_file(file_path: Path) -> list[Message]: try: with open(file_path, "r") as f: messages = [json.loads(m) for m in list(f) if m.strip()] @@ -32,7 +40,7 @@ def read_from_file(file_path: Path) -> List[Message]: return [Message(**m) for m in messages] -def list_sorted_session_files(session_files_directory: Path) -> Dict[str, Path]: +def list_sorted_session_files(session_files_directory: Path) -> dict[str, Path]: logs = list_session_files(session_files_directory) return {log.stem: log for log in sorted(logs, key=lambda x: x.stat().st_mtime, reverse=True)} @@ -47,7 +55,7 @@ def session_file_exists(session_files_directory: Path) -> bool: return any(list_session_files(session_files_directory)) -def save_latest_session(file_path: Path, messages: List[Message]) -> None: +def save_latest_session(file_path: Path, messages: list[Message]) -> None: with tempfile.NamedTemporaryFile("w", delete=False) as temp_file: _write_messages_to_file(temp_file, messages) temp_file_path = temp_file.name @@ -55,7 +63,7 @@ def save_latest_session(file_path: Path, messages: List[Message]) -> None: os.replace(temp_file_path, file_path) -def _write_messages_to_file(file: any, messages: List[Message]) -> None: +def _write_messages_to_file(file: any, messages: list[Message]) -> None: for m in messages: json.dump(m.to_dict(), file) file.write("\n") diff --git a/tests/cli/prompt/test_overwrite_session_prompt.py b/tests/cli/prompt/test_overwrite_session_prompt.py new file mode 100644 index 000000000..95cf825b4 --- /dev/null +++ b/tests/cli/prompt/test_overwrite_session_prompt.py @@ -0,0 +1,49 @@ +import pytest +from goose.cli.prompt.overwrite_session_prompt import OverwriteSessionPrompt + + +@pytest.fixture +def prompt(): + return OverwriteSessionPrompt() + + +def test_init(prompt): + assert prompt.choices == { + "yes": "Overwrite the existing session", + "no": "Pick a new session name", + "resume": "Resume the existing session", + } + assert prompt.default == "resume" + + +@pytest.mark.parametrize( + "choice, expected", + [ + ("", False), + ("invalid", False), + ("n", True), + ("N", True), + ("no", True), + ("NO", True), + ("r", True), + ("R", True), + ("resume", True), + ("RESUME", True), + ("y", True), + ("Y", True), + ("yes", True), + ("YES", True), + ], +) +def test_check_choice(prompt, choice, expected): + assert prompt.check_choice(choice) == expected + + +def test_instantiation(): + prompt = OverwriteSessionPrompt() + assert prompt.choices == { + "yes": "Overwrite the existing session", + "no": "Pick a new session name", + "resume": "Resume the existing session", + } + assert prompt.default == "resume" diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py index 9f6f2f66f..38b38920f 100644 --- a/tests/cli/test_main.py +++ b/tests/cli/test_main.py @@ -34,7 +34,9 @@ def test_session_start_command_with_session_name(mock_session): mock_session_class, mock_session_instance = mock_session runner = CliRunner() runner.invoke(goose_cli, ["session", "start", "session1", "--profile", "default"]) - mock_session_class.assert_called_once_with(name="session1", profile="default", plan=None, log_level="INFO") + mock_session_class.assert_called_once_with( + name="session1", profile="default", plan=None, log_level="INFO", tracing=False + ) mock_session_instance.run.assert_called_once() diff --git a/tests/cli/test_session.py b/tests/cli/test_session.py index f2437462c..b2eafea6c 100644 --- a/tests/cli/test_session.py +++ b/tests/cli/test_session.py @@ -1,7 +1,8 @@ +import json from unittest.mock import MagicMock, patch import pytest -from exchange import Exchange, Message, ToolUse, ToolResult +from exchange import Exchange, Message, ToolResult, ToolUse from goose.cli.prompt.goose_prompt_session import GoosePromptSession from goose.cli.prompt.user_input import PromptAction, UserInput from goose.cli.session import Session @@ -123,36 +124,88 @@ def test_log_log_cost(create_session_with_mock_configs): mock_logger.info.assert_called_once_with(cost_message) -def test_run_should_auto_save_session(create_session_with_mock_configs, mock_sessions_path): +@patch.object(GoosePromptSession, "get_user_input", name="get_user_input") +@patch.object(Exchange, "generate", name="mock_generate") +@patch("goose.cli.session.save_latest_session", name="mock_save_latest_session") +def test_run_should_auto_save_session( + mock_save_latest_session, + mock_generate, + mock_get_user_input, + create_session_with_mock_configs, + mock_sessions_path, +): def custom_exchange_generate(self, *args, **kwargs): message = Message.assistant("Response") self.add(message) return message + def mock_generate_side_effect(*args, **kwargs): + return custom_exchange_generate(session.exchange, *args, **kwargs) + + def save_latest_session(file, messages): + file.write_text("\n".join(json.dumps(m.to_dict()) for m in messages)) + user_inputs = [ UserInput(action=PromptAction.CONTINUE, text="Question1"), UserInput(action=PromptAction.CONTINUE, text="Question2"), UserInput(action=PromptAction.EXIT), ] - session = create_session_with_mock_configs({"name": SESSION_NAME}) - with ( - patch.object(GoosePromptSession, "get_user_input", side_effect=user_inputs), - patch.object(Exchange, "generate") as mock_generate, - patch("goose.cli.session.save_latest_session") as mock_save_latest_session, - ): - mock_generate.side_effect = lambda *args, **kwargs: custom_exchange_generate(session.exchange, *args, **kwargs) - session.run() - session_file = mock_sessions_path / f"{SESSION_NAME}.jsonl" - assert session.exchange.generate.call_count == 2 - assert mock_save_latest_session.call_count == 2 - assert mock_save_latest_session.call_args_list[0][0][0] == session_file - assert session_file.exists() + mock_get_user_input.side_effect = user_inputs + mock_generate.side_effect = mock_generate_side_effect + mock_save_latest_session.side_effect = save_latest_session + + session.run() + + session_file = mock_sessions_path / f"{SESSION_NAME}.jsonl" + + assert mock_generate.call_count == 2 + assert mock_save_latest_session.call_count == 2 + assert mock_save_latest_session.call_args_list[0][0][0] == session_file + assert session_file.exists() + with open(session_file, "r") as f: + saved_messages = [json.loads(line) for line in f] + + expected_messages = [ + Message.user("Question1"), + Message.assistant("Response"), + Message.user("Question2"), + Message.assistant("Response"), + ] + + assert len(saved_messages) == len(expected_messages) + for saved, expected in zip(saved_messages, expected_messages): + assert saved["role"] == expected.role + assert saved["content"][0]["text"] == expected.text + + +@patch("goose.cli.session.droid", return_value="generated_session_name", name="mock_droid") +def test_set_generated_session_name(mock_droid, create_session_with_mock_configs, mock_sessions_path): + session = create_session_with_mock_configs({"name": None}) + assert session.name == "generated_session_name" + + +@patch("goose.cli.session.is_existing_session", name="mock_is_existing") +@patch("goose.cli.session.Session._prompt_overwrite_session", name="mock_prompt") +def test_existing_session_prompt(mock_prompt, mock_is_existing, create_session_with_mock_configs): + session = create_session_with_mock_configs({"name": SESSION_NAME}) -def test_set_generated_session_name(create_session_with_mock_configs, mock_sessions_path): - generated_session_name = "generated_session_name" - with patch("goose.cli.session.droid", return_value=generated_session_name): - session = create_session_with_mock_configs({"name": None}) - assert session.name == generated_session_name + def check_prompt_behavior(is_existing, new_session, should_prompt): + mock_is_existing.return_value = is_existing + if new_session is None: + session.run() + else: + session.run(new_session=new_session) + + if should_prompt: + mock_prompt.assert_called_once() + else: + mock_prompt.assert_not_called() + mock_prompt.reset_mock() + + check_prompt_behavior(is_existing=True, new_session=None, should_prompt=True) + check_prompt_behavior(is_existing=False, new_session=None, should_prompt=False) + check_prompt_behavior(is_existing=True, new_session=True, should_prompt=True) + check_prompt_behavior(is_existing=False, new_session=False, should_prompt=False) diff --git a/tests/utils/test_session_file.py b/tests/utils/test_session_file.py index 6a2a64981..290564566 100644 --- a/tests/utils/test_session_file.py +++ b/tests/utils/test_session_file.py @@ -1,9 +1,11 @@ import os from pathlib import Path +from unittest.mock import patch import pytest from exchange import Message from goose.utils.session_file import ( + is_empty_session, list_sorted_session_files, read_from_file, read_or_create_file, @@ -115,3 +117,22 @@ def create_session_file(file_path, file_name) -> Path: file = file_path / f"{file_name}.jsonl" file.touch() return file + + +@patch("pathlib.Path.is_file", return_value=True, name="mock_is_file") +@patch("pathlib.Path.stat", name="mock_stat") +def test_is_empty_session(mock_stat, mock_is_file): + mock_stat.return_value.st_size = 0 + assert is_empty_session(Path("empty_file.json")) + + +@patch("pathlib.Path.is_file", return_value=True, name="mock_is_file") +@patch("pathlib.Path.stat", name="mock_stat") +def test_is_not_empty_session(mock_stat, mock_is_file): + mock_stat.return_value.st_size = 100 + assert not is_empty_session(Path("non_empty_file.json")) + + +@patch("pathlib.Path.is_file", return_value=False, name="mock_is_file") +def test_is_not_empty_session_file_not_found(mock_is_file): + assert not is_empty_session(Path("file_not_found.json"))