From e93688598550e81f9aeebde02f8387974feaa2d6 Mon Sep 17 00:00:00 2001 From: Dirk Brand Date: Mon, 16 Dec 2024 14:01:52 +0200 Subject: [PATCH 1/5] First iteration of the game generator --- cookbook/run_qdrant.sh | 5 ++ cookbook/workflows/.gitignore | 1 + cookbook/workflows/05_playground.py | 10 ++- cookbook/workflows/game_generator.py | 127 +++++++++++++++++++++++++++ phi/agent/agent.py | 2 +- phi/embedder/ollama.py | 5 +- phi/utils/string.py | 17 ++++ 7 files changed, 162 insertions(+), 5 deletions(-) create mode 100755 cookbook/run_qdrant.sh create mode 100644 cookbook/workflows/game_generator.py create mode 100644 phi/utils/string.py diff --git a/cookbook/run_qdrant.sh b/cookbook/run_qdrant.sh new file mode 100755 index 000000000..08c8a9ca2 --- /dev/null +++ b/cookbook/run_qdrant.sh @@ -0,0 +1,5 @@ +docker run -p 6333:6333 \ + -d \ + -v qdrant-volume:/qdrant/storage \ + --name qdrant \ + qdrant/qdrant diff --git a/cookbook/workflows/.gitignore b/cookbook/workflows/.gitignore index 27a3afbbc..0cb64414d 100644 --- a/cookbook/workflows/.gitignore +++ b/cookbook/workflows/.gitignore @@ -1 +1,2 @@ reports +games diff --git a/cookbook/workflows/05_playground.py b/cookbook/workflows/05_playground.py index c7554d127..d40b4e9d9 100644 --- a/cookbook/workflows/05_playground.py +++ b/cookbook/workflows/05_playground.py @@ -2,7 +2,7 @@ 1. Install dependencies using: `pip install openai duckduckgo-search sqlalchemy 'fastapi[standard]' newspaper4k lxml_html_clean yfinance phidata` 2. Run the script using: `python cookbook/workflows/05_playground.py` """ - +from cookbook.workflows.game_generator import GameGenerator from phi.playground import Playground, serve_playground_app from phi.storage.workflow.sqlite import SqlWorkflowStorage @@ -35,6 +35,14 @@ ), ) +game_generator = GameGenerator( + workflow_id="game-generator", + storage=SqlWorkflowStorage( + table_name="game_generator_workflows", + db_file="tmp/workflows.db", + ), +) + # Initialize the Playground with the workflows app = Playground(workflows=[blog_post_generator, news_report_generator, investment_report_generator]).get_app() diff --git a/cookbook/workflows/game_generator.py b/cookbook/workflows/game_generator.py new file mode 100644 index 000000000..01f0bc4b9 --- /dev/null +++ b/cookbook/workflows/game_generator.py @@ -0,0 +1,127 @@ +""" +1. Install dependencies using: `pip install openai yfinance phidata` +2. Run the script using: `python cookbook/workflows/investment_report_generator.py` +""" +import json +from pathlib import Path +from typing import Iterator + +from pydantic import BaseModel, Field + +from phi.agent import Agent, RunResponse +from phi.model.openai import OpenAIChat +from phi.run.response import RunEvent +from phi.storage.workflow.sqlite import SqlWorkflowStorage +from phi.utils.log import logger +from phi.utils.pprint import pprint_run_response +from phi.utils.string import hash_string_sha256 +from phi.workflow import Workflow + + +games_dir = Path(__file__).parent.joinpath("games") +games_dir.mkdir(parents=True, exist_ok=True) +game_output_path = games_dir / "game_output_file.html" +game_output_path.unlink(missing_ok=True) + + +class GameOutput(BaseModel): + reasoning: str = Field(..., description="Explain your reasoning") + code: str = Field(..., description="The html5 code for the game") + instructions: str = Field(..., description="Instructions how to play the game") + + +class QAOutput(BaseModel): + reasoning: str = Field(..., description="Explain your reasoning") + correct: bool = Field(False, description="Does the game pass your criteria?") + + +class GameGenerator(Workflow): + # This description is only used in the workflow UI + description: str = "Generator for single-page HTML5 games" + + game_developer: Agent = Agent( + name="Game Developer Agent", + description="You are a game developer that produces working HTML5 code.", + model=OpenAIChat(id="gpt-4o"), + instructions=[ + "Create a game based on the user's prompt. " + "The game should be HTML5, completely self-contained and must be runnable simply by opening on a browser", + "Ensure the game has a alert that pops up if the user dies and then allows the user to restart or exit the game.", + "Ensure instructions for the game are displayed on the HTML page." + "Use user-friendly colours and make the game canvas large enough for the game to be playable on a larger screen." + ], + response_model=GameOutput, + ) + + qa_agent: Agent = Agent( + name="QA Agent", + model=OpenAIChat(id="gpt-4o"), + description="You are a game QA and you evaluate html5 code for correctness.", + instructions=[ + "You will be given some HTML5 code." + "Your task is to read the code and evaluate it for correctness, but also that it matches the original task description.", + ], + response_model=QAOutput, + ) + + def run(self, game_description: str) -> Iterator[RunResponse]: + logger.info(f"Game description: {game_description}") + + game_output = self.game_developer.run(game_description) + + if game_output and game_output.content and isinstance(game_output.content, GameOutput): + game_code = game_output.content.code + logger.info(f"Game code: {game_code}") + else: + yield RunResponse( + run_id=self.run_id, event=RunEvent.workflow_completed, content="Sorry, could not generate a game." + ) + return + + logger.info("QA'ing the game code") + qa_input = { + "game_description": game_description, + "game_code": game_code, + } + qa_output = self.qa_agent.run(json.dumps(qa_input, indent=2)) + + if qa_output and qa_output.content and isinstance(qa_output.content, QAOutput): + logger.info(qa_output.content) + if not qa_output.content.correct: + raise Exception(f"QA failed for code: {game_code}") + + # Store the resulting code + game_output_path.write_text(game_code) + + yield RunResponse(run_id=self.run_id, event=RunEvent.workflow_completed, content=game_output.content.instructions) + else: + yield RunResponse( + run_id=self.run_id, event=RunEvent.workflow_completed, content="Sorry, could not QA the game." + ) + return + + +# Run the workflow if the script is executed directly +if __name__ == "__main__": + from rich.prompt import Prompt + + game_description = Prompt.ask( + "[bold]Describe the game you want to make (keep it simple)[/bold]\n✨", default="A simple snake game" + ) + + hash_of_description = hash_string_sha256(game_description) + + # Initialize the investment analyst workflow + game_generator = GameGenerator( + session_id=f"game-gen-{hash_of_description}", + storage=SqlWorkflowStorage( + table_name="game_generator_workflows", + db_file="tmp/workflows.db", + ), + ) + + # Execute the workflow + game_code: Iterator[RunResponse] = game_generator.run(game_description=game_description) + + # Print the report + pprint_run_response(game_code) diff --git a/phi/agent/agent.py b/phi/agent/agent.py index 2b7ce303e..dfb4d12bb 100644 --- a/phi/agent/agent.py +++ b/phi/agent/agent.py @@ -495,7 +495,7 @@ def update_model(self) -> None: "Please provide a `model` or install `openai`." ) exit(1) - self.model = OpenAIChat() + self.model = OpenAIChat() # We default to OpenAIChat as a base model # Set response_format if it is not set on the Model if self.response_model is not None and self.model.response_format is None: diff --git a/phi/embedder/ollama.py b/phi/embedder/ollama.py index 75c16ab67..1b2e8f9bc 100644 --- a/phi/embedder/ollama.py +++ b/phi/embedder/ollama.py @@ -5,9 +5,8 @@ try: from ollama import Client as OllamaClient -except ImportError: - logger.error("`ollama` not installed") - raise +except (ModuleNotFoundError, ImportError): + raise ImportError("`ollama` not installed. Please install using `pip install ollama`") class OllamaEmbedder(Embedder): diff --git a/phi/utils/string.py b/phi/utils/string.py new file mode 100644 index 000000000..3823e00f6 --- /dev/null +++ b/phi/utils/string.py @@ -0,0 +1,17 @@ +import hashlib + + +def hash_string_sha256(input_string): + # Encode the input string to bytes + encoded_string = input_string.encode("utf-8") + + # Create a SHA-256 hash object + sha256_hash = hashlib.sha256() + + # Update the hash object with the encoded string + sha256_hash.update(encoded_string) + + # Get the hexadecimal digest of the hash + hex_digest = sha256_hash.hexdigest() + + return hex_digest From 2638dd5b6d0bcf225b28bcfb0c3b0d5199f46eec Mon Sep 17 00:00:00 2001 From: Dirk Brand Date: Mon, 16 Dec 2024 14:06:59 +0200 Subject: [PATCH 2/5] Update --- cookbook/workflows/05_playground.py | 1 + cookbook/workflows/game_generator.py | 7 +++++-- phi/vectordb/qdrant/qdrant.py | 2 +- phi/workspace/config.py | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cookbook/workflows/05_playground.py b/cookbook/workflows/05_playground.py index d40b4e9d9..c7066e5c2 100644 --- a/cookbook/workflows/05_playground.py +++ b/cookbook/workflows/05_playground.py @@ -2,6 +2,7 @@ 1. Install dependencies using: `pip install openai duckduckgo-search sqlalchemy 'fastapi[standard]' newspaper4k lxml_html_clean yfinance phidata` 2. Run the script using: `python cookbook/workflows/05_playground.py` """ + from cookbook.workflows.game_generator import GameGenerator from phi.playground import Playground, serve_playground_app from phi.storage.workflow.sqlite import SqlWorkflowStorage diff --git a/cookbook/workflows/game_generator.py b/cookbook/workflows/game_generator.py index 01f0bc4b9..b5d5679f5 100644 --- a/cookbook/workflows/game_generator.py +++ b/cookbook/workflows/game_generator.py @@ -2,6 +2,7 @@ 1. Install dependencies using: `pip install openai yfinance phidata` 2. Run the script using: `python cookbook/workflows/investment_report_generator.py` """ + import json from pathlib import Path from typing import Iterator @@ -48,7 +49,7 @@ class GameGenerator(Workflow): "The game should be HTML5, completely self-contained and must be runnable simply by opening on a browser", "Ensure the game has a alert that pops up if the user dies and then allows the user to restart or exit the game.", "Ensure instructions for the game are displayed on the HTML page." - "Use user-friendly colours and make the game canvas large enough for the game to be playable on a larger screen." + "Use user-friendly colours and make the game canvas large enough for the game to be playable on a larger screen.", ], response_model=GameOutput, ) @@ -93,7 +94,9 @@ def run(self, game_description: str) -> Iterator[RunResponse]: # Store the resulting code game_output_path.write_text(game_code) - yield RunResponse(run_id=self.run_id, event=RunEvent.workflow_completed, content=game_output.content.instructions) + yield RunResponse( + run_id=self.run_id, event=RunEvent.workflow_completed, content=game_output.content.instructions + ) else: yield RunResponse( run_id=self.run_id, event=RunEvent.workflow_completed, content="Sorry, could not QA the game." diff --git a/phi/vectordb/qdrant/qdrant.py b/phi/vectordb/qdrant/qdrant.py index 79a7d386c..ceefda1f0 100644 --- a/phi/vectordb/qdrant/qdrant.py +++ b/phi/vectordb/qdrant/qdrant.py @@ -83,7 +83,7 @@ def client(self) -> QdrantClient: https=self.https, api_key=self.api_key, prefix=self.prefix, - timeout=self.timeout, + timeout=int(self.timeout) if self.timeout is not None else None, host=self.host, path=self.path, **self.kwargs, diff --git a/phi/workspace/config.py b/phi/workspace/config.py index 241bcfc60..833a94840 100644 --- a/phi/workspace/config.py +++ b/phi/workspace/config.py @@ -19,6 +19,7 @@ def get_workspace_objects_from_file(resource_file: Path) -> dict: """Returns workspace objects from the resource file""" from phi.aws.resources import AwsResources from phi.docker.resources import DockerResources + try: python_objects = get_python_objects_from_module(resource_file) # logger.debug(f"python_objects: {python_objects}") From 9dddfd8e1dea084965a92100178118c68681e505 Mon Sep 17 00:00:00 2001 From: Dirk Brand Date: Mon, 16 Dec 2024 14:33:27 +0200 Subject: [PATCH 3/5] Update --- cookbook/workflows/game_generator.py | 13 ++++++++++--- phi/utils/web.py | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 phi/utils/web.py diff --git a/cookbook/workflows/game_generator.py b/cookbook/workflows/game_generator.py index b5d5679f5..f9fa96054 100644 --- a/cookbook/workflows/game_generator.py +++ b/cookbook/workflows/game_generator.py @@ -10,12 +10,14 @@ from pydantic import BaseModel, Field from phi.agent import Agent, RunResponse +from phi.model.anthropic import Claude from phi.model.openai import OpenAIChat from phi.run.response import RunEvent from phi.storage.workflow.sqlite import SqlWorkflowStorage from phi.utils.log import logger from phi.utils.pprint import pprint_run_response from phi.utils.string import hash_string_sha256 +from phi.utils.web import open_html_file from phi.workflow import Workflow @@ -109,7 +111,9 @@ def run(self, game_description: str) -> Iterator[RunResponse]: from rich.prompt import Prompt game_description = Prompt.ask( - "[bold]Describe the game you want to make (keep it simple)[/bold]\n✨", default="A simple snake game" + "[bold]Describe the game you want to make (keep it simple)[/bold]\n✨", + # default="An asteroids game." + default="An asteroids game. Make sure the asteroids move randomly and are random sizes. They should continually spawn more and become more difficult over time. Keep score. Make my spaceship's movement realistic." ) hash_of_description = hash_string_sha256(game_description) @@ -124,7 +128,10 @@ def run(self, game_description: str) -> Iterator[RunResponse]: ) # Execute the workflow - game_code: Iterator[RunResponse] = game_generator.run(game_description=game_description) + result: Iterator[RunResponse] = game_generator.run(game_description=game_description) # Print the report - pprint_run_response(game_code) + pprint_run_response(result) + + if game_output_path.exists(): + open_html_file(game_output_path) diff --git a/phi/utils/web.py b/phi/utils/web.py new file mode 100644 index 000000000..9983736b8 --- /dev/null +++ b/phi/utils/web.py @@ -0,0 +1,26 @@ +import webbrowser +from pathlib import Path + +from phi.utils.log import logger + + +def open_html_file(file_path: Path): + """ + Opens the specified HTML file in the default web browser. + + :param file_path: Path to the HTML file. + """ + # Resolve the absolute path + absolute_path = file_path.resolve() + + if not absolute_path.is_file(): + logger.error(f"The file '{absolute_path}' does not exist.") + raise FileNotFoundError(f"The file '{absolute_path}' does not exist.") + + # Convert the file path to a file URI + file_url = absolute_path.as_uri() + + # Open the file in the default web browser + webbrowser.open(file_url) + + From 952e187b6febaf128a46a2353c1aa55cc14779c0 Mon Sep 17 00:00:00 2001 From: Dirk Brand Date: Mon, 16 Dec 2024 14:36:42 +0200 Subject: [PATCH 4/5] Fix style --- cookbook/workflows/game_generator.py | 3 +-- phi/utils/web.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/cookbook/workflows/game_generator.py b/cookbook/workflows/game_generator.py index f9fa96054..131a8de16 100644 --- a/cookbook/workflows/game_generator.py +++ b/cookbook/workflows/game_generator.py @@ -10,7 +10,6 @@ from pydantic import BaseModel, Field from phi.agent import Agent, RunResponse -from phi.model.anthropic import Claude from phi.model.openai import OpenAIChat from phi.run.response import RunEvent from phi.storage.workflow.sqlite import SqlWorkflowStorage @@ -113,7 +112,7 @@ def run(self, game_description: str) -> Iterator[RunResponse]: game_description = Prompt.ask( "[bold]Describe the game you want to make (keep it simple)[/bold]\n✨", # default="An asteroids game." - default="An asteroids game. Make sure the asteroids move randomly and are random sizes. They should continually spawn more and become more difficult over time. Keep score. Make my spaceship's movement realistic." + default="An asteroids game. Make sure the asteroids move randomly and are random sizes. They should continually spawn more and become more difficult over time. Keep score. Make my spaceship's movement realistic.", ) hash_of_description = hash_string_sha256(game_description) diff --git a/phi/utils/web.py b/phi/utils/web.py index 9983736b8..e3dab93fb 100644 --- a/phi/utils/web.py +++ b/phi/utils/web.py @@ -22,5 +22,3 @@ def open_html_file(file_path: Path): # Open the file in the default web browser webbrowser.open(file_url) - - From 19aff9a257c93044acffe15f0e4adcfed40cd369 Mon Sep 17 00:00:00 2001 From: Dirk Brand Date: Mon, 16 Dec 2024 17:40:11 +0200 Subject: [PATCH 5/5] Improve docstrings --- ...stamps.py => 44_generate_yt_timestamps.py} | 0 .../fal_tools.py} | 40 +++++++++---------- .../replicate_tools.py} | 0 phi/tools/baidusearch.py | 2 +- phi/tools/duckduckgo.py | 12 ++++++ phi/tools/linear_tools.py | 3 +- phi/tools/replicate.py | 4 +- 7 files changed, 37 insertions(+), 24 deletions(-) rename cookbook/agents/{46_generate_yt_timestamps.py => 44_generate_yt_timestamps.py} (100%) rename cookbook/{agents/45_generate_fal_video.py => tools/fal_tools.py} (97%) rename cookbook/{agents/44_generate_replicate_image.py => tools/replicate_tools.py} (100%) diff --git a/cookbook/agents/46_generate_yt_timestamps.py b/cookbook/agents/44_generate_yt_timestamps.py similarity index 100% rename from cookbook/agents/46_generate_yt_timestamps.py rename to cookbook/agents/44_generate_yt_timestamps.py diff --git a/cookbook/agents/45_generate_fal_video.py b/cookbook/tools/fal_tools.py similarity index 97% rename from cookbook/agents/45_generate_fal_video.py rename to cookbook/tools/fal_tools.py index 8ed8139b4..3342ccfbc 100644 --- a/cookbook/agents/45_generate_fal_video.py +++ b/cookbook/tools/fal_tools.py @@ -1,20 +1,20 @@ -from phi.agent import Agent -from phi.model.openai import OpenAIChat -from phi.tools.fal_tools import FalTools - -fal_agent = Agent( - name="Fal Video Generator Agent", - model=OpenAIChat(id="gpt-4o"), - tools=[FalTools("fal-ai/hunyuan-video")], - description="You are an AI agent that can generate videos using the Fal API.", - instructions=[ - "When the user asks you to create a video, use the `generate_media` tool to create the video.", - "Return the URL as raw to the user.", - "Don't convert video URL to markdown or anything else.", - ], - markdown=True, - debug_mode=True, - show_tool_calls=True, -) - -fal_agent.print_response("Generate video of balloon in the ocean") +from phi.agent import Agent +from phi.model.openai import OpenAIChat +from phi.tools.fal_tools import FalTools + +fal_agent = Agent( + name="Fal Video Generator Agent", + model=OpenAIChat(id="gpt-4o"), + tools=[FalTools("fal-ai/hunyuan-video")], + description="You are an AI agent that can generate videos using the Fal API.", + instructions=[ + "When the user asks you to create a video, use the `generate_media` tool to create the video.", + "Return the URL as raw to the user.", + "Don't convert video URL to markdown or anything else.", + ], + markdown=True, + debug_mode=True, + show_tool_calls=True, +) + +fal_agent.print_response("Generate video of balloon in the ocean") diff --git a/cookbook/agents/44_generate_replicate_image.py b/cookbook/tools/replicate_tools.py similarity index 100% rename from cookbook/agents/44_generate_replicate_image.py rename to cookbook/tools/replicate_tools.py diff --git a/phi/tools/baidusearch.py b/phi/tools/baidusearch.py index d87e8fc4f..263feff0f 100644 --- a/phi/tools/baidusearch.py +++ b/phi/tools/baidusearch.py @@ -22,7 +22,7 @@ class BaiduSearch(Toolkit): Args: fixed_max_results (Optional[int]): A fixed number of maximum results. fixed_language (Optional[str]): A fixed language for the search results. - headers (Optional[Any]): Headers to be used in the search request. + headers (Optional[Any]): proxy (Optional[str]): Proxy to be used in the search request. debug (Optional[bool]): Enable debug output. """ diff --git a/phi/tools/duckduckgo.py b/phi/tools/duckduckgo.py index 7f05176e9..3a5787646 100644 --- a/phi/tools/duckduckgo.py +++ b/phi/tools/duckduckgo.py @@ -11,6 +11,18 @@ class DuckDuckGo(Toolkit): + """ + DuckDuckGo is a toolkit for searching DuckDuckGo easily. + + Args: + search (bool): Enable DuckDuckGo search function. + news (bool): Enable DuckDuckGo news function. + fixed_max_results (Optional[int]): A fixed number of maximum results. + headers (Optional[Any]): + proxy (Optional[str]): Proxy to be used in the search request. + proxies (Optional[Any]): A list of proxies to be used in the search request. + timeout (Optional[int]): The maximum number of seconds to wait for a response. + """ def __init__( self, search: bool = True, diff --git a/phi/tools/linear_tools.py b/phi/tools/linear_tools.py index 835c0f619..e45cda704 100644 --- a/phi/tools/linear_tools.py +++ b/phi/tools/linear_tools.py @@ -22,7 +22,6 @@ def __init__( if not self.api_token: api_error_message = "API token 'LINEAR_API_KEY' is missing. Please set it as an environment variable." logger.error(api_error_message) - raise ValueError(api_error_message) self.endpoint = "https://api.linear.app/graphql" self.headers = {"Authorization": f"{self.api_token}"} @@ -359,7 +358,7 @@ def get_high_priority_issues(self) -> Optional[str]: query = """ query HighPriorityIssues { - issues(filter: { + issues(filter: { priority: { lte: 2 } }) { nodes { diff --git a/phi/tools/replicate.py b/phi/tools/replicate.py index 7d5fb3e16..f0548cf84 100644 --- a/phi/tools/replicate.py +++ b/phi/tools/replicate.py @@ -1,5 +1,6 @@ import os from os import getenv +from typing import Optional from urllib.parse import urlparse from uuid import uuid4 @@ -18,10 +19,11 @@ class ReplicateTools(Toolkit): def __init__( self, + api_key: Optional[str] = None, model: str = "minimax/video-01", ): super().__init__(name="replicate_toolkit") - self.api_key = getenv("REPLICATE_API_TOKEN") + self.api_key = api_key or getenv("REPLICATE_API_TOKEN") if not self.api_key: logger.error("REPLICATE_API_TOKEN not set. Please set the REPLICATE_API_TOKEN environment variable.") self.model = model