From b79c8dabb9a4901d84413f1c8ab613012dd24efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Tue, 2 Apr 2024 11:31:23 +0800 Subject: [PATCH] feat: +intent detect --- metagpt/actions/intent_detect.py | 397 ++++++++++++++++++++ tests/metagpt/actions/test_intent_detect.py | 171 +++++++++ 2 files changed, 568 insertions(+) create mode 100644 metagpt/actions/intent_detect.py create mode 100644 tests/metagpt/actions/test_intent_detect.py diff --git a/metagpt/actions/intent_detect.py b/metagpt/actions/intent_detect.py new file mode 100644 index 0000000000..056125618c --- /dev/null +++ b/metagpt/actions/intent_detect.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This script is designed to classify intentions from complete conversation content. + +Usage: + This script can be used to classify intentions from a conversation. It utilizes models for detecting intentions + from the text provided and categorizes them accordingly. If the intention of certain words or phrases is unclear, + it prompts the user for clarification. + +Dependencies: + This script depends on the metagpt library, pydantic, and other utilities for message parsing and interaction. + +""" +import json +from typing import List + +from pydantic import BaseModel, Field + +from metagpt.actions import Action +from metagpt.logs import logger +from metagpt.schema import Message +from metagpt.utils.common import parse_json_code_block + + +class SOPItem(BaseModel): + """ + Represents an item in a Standard Operating Procedure (SOP). + + Attributes: + description (str): The description or title of the SOP. + sop (List[str]): The steps or details of the SOP. + """ + + description: str + sop: List[str] + + +SOP_CONFIG = [ + SOPItem( + description="Intentions related to or including software development, such as developing or building software, games, programming, app, websites, etc. Excluding bug fixes, report any issues.", + sop=[ + "Writes a PRD based on software requirements.", + "Writes a design to the project repository, based on the PRD of the project.", + "Writes a project plan to the project repository, based on the design of the project.", + "Writes code to implement designed features according to the project plan and adds them to the project repository.", + # "Run QA test on the project repository.", + "Stage and commit changes for the project repository using Git.", + ], + ), + SOPItem( + description="Error message, issues, fix bug, exception description", + sop=[ + "Fix bugs in the project repository.", + "Stage and commit changes for the project repository using Git.", + ], + ), + SOPItem( + description="download repository from git and format the project to MetaGPT project", + sop=[ + "Imports a project from a Git website and formats it to MetaGPT project format to enable incremental appending requirements.", + "Stage and commit changes for the project repository using Git.", + ], + ), +] + + +class IntentDetectClarification(BaseModel): + """ + Represents clarifications for unclear intentions. + + Attributes: + ref (str): The reference to the original words. + clarification (str): A question for the user to clarify the intention of the unclear words. + """ + + ref: str + clarification: str + + +class IntentDetectIntentionRef(BaseModel): + """ + Represents intentions along with their references. + + Attributes: + intent (str): The intention from the "Intentions" section. + refs (List[str]): List of original text references from the "Dialog" section that match the intention. + """ + + intent: str + refs: List[str] + + +class IntentDetectIntentionSOP(BaseModel): + """ + Represents an intention mapped to a Standard Operating Procedure (SOP). + + Attributes: + intention (IntentDetectIntentionRef): Reference to the intention. + sop (SOPItem, optional): Standard Operating Procedure (SOP) item related to the intention. + """ + + intention: IntentDetectIntentionRef + sop: SOPItem = None + + +class IntentDetectResult(BaseModel): + """ + Represents the result of intention detection. + + Attributes: + clarifications (List[IntentDetectClarification]): List of clarifications for unclear intentions. + intentions (List[IntentDetectIntentionSOP]): List of intentions mapped to Standard Operating Procedures (SOPs). + """ + + clarifications: List[IntentDetectClarification] = Field(default_factory=list) + intentions: List[IntentDetectIntentionSOP] = Field(default_factory=list) + + +class IntentDetect(Action): + """ + Action class for intention detection. + + Attributes: + _dialog_intentions (IntentDetectDialogIntentions): Instance of IntentDetectDialogIntentions. + Dialog intentions for matching user intentions. + _references (IntentDetectReferences): Instance of IntentDetectReferences. + References to intentions and unreferenced content. + _intent_to_sops (List[IntentSOP]): List of IntentSOP objects. + Mapping of intentions to Standard Operating Procedures (SOPs). + result (IntentDetectResult): Instance of IntentDetectResult. + Result object containing the outcome of intention detection. + """ + + class IntentDetectDialogIntentions(BaseModel): + class IntentDetectIntention(BaseModel): + ref: str + intent: str + + intentions: List[IntentDetectIntention] + clarifications: List[IntentDetectClarification] + + class IntentDetectReferences(BaseModel): + class IntentDetectUnrefs(BaseModel): + ref: str + reason: str + + intentions: List[IntentDetectIntentionRef] + unrefs: List[IntentDetectUnrefs] + + class IntentSOP(BaseModel): + intent: str + sop: str + sop_index: int + reason: str + + _dialog_intentions: IntentDetectDialogIntentions = None + _references: IntentDetectReferences = None + _intent_to_sops: List[IntentSOP] = None + result: IntentDetectResult = None + + async def run(self, with_messages: List[Message] = None, **kwargs) -> Message: + """ + Runs the intention detection action. + + Args: + with_messages (List[Message]): List of messages representing the conversation content. + **kwargs: Additional keyword arguments. + """ + msg_markdown = self._message_to_markdown(with_messages) + intentions = await self._get_intentions(msg_markdown) + await self._get_references(msg_markdown, intentions) + await self._get_sops() + await self._merge() + + return Message( + content=self.result.model_dump_json(), role="assistant", cause_by=self, instruct_content=self.result + ) + + async def _get_intentions(self, msg_markdown: str) -> List[str]: + rsp = await self.llm.aask( + msg_markdown, + system_msgs=[ + "You are a tool that can classify user intentions.", + "Detect and classify the intention of each word spoken by the user in the conversation.", + "If the user's intention is not clear, create a request for the user to clarify the intention of" + " the unclear words.", + "Return a markdown object with:\n" + '- an "intentions" key containing a list of JSON objects, where each object contains:\n' + ' - a "ref" key containing the original words reference;\n' + ' - an "intent" key explaining the intention of the referenced word;\n' + '- a "clarifications" key containing a list of JSON objects, where each object contains:\n' + ' - a "ref" key containing the original words reference;\n' + ' - a "clarification" key containing a question, in the tone of an assistant, prompts the user to provide more details about the intention regarding the unclear word(s) referenced in the user\'s description.', + ], + stream=False, + ) + logger.debug(rsp) + json_blocks = parse_json_code_block(rsp) + if not json_blocks: + return [] + self._dialog_intentions = self.IntentDetectDialogIntentions.model_validate_json(json_blocks[0]) + return [i.intent for i in self._dialog_intentions.intentions] + + async def _get_references(self, msg_markdown: str, intentions: List[str]): + intention_list = "\n".join([f"- {i}" for i in intentions]) + prompt = f"## Dialog\n{msg_markdown}\n---\n## Intentions\n{intention_list}\n" + rsp = await self.llm.aask( + prompt, + system_msgs=[ + "You are a tool that categorizes text content by intent.", + "Place the original text from the `Dialog` section under the matching intent of `Intentions` section.", + "Allow different intents to reference the same original text.", + "Return a markdown JSON object with:\n" + '- an "intentions" key containing a list of JSON objects, where each object contains:\n' + ' - a "intent" key containing the intention from "Intentions" section;\n' + ' - a "refs" key containing a list of strings of original text from the "Dialog" section that match' + " the intention.\n" + '- a "unrefs" key containing a list of JSON objects, where each object contains:\n' + ' - a "ref" key containing the unreferenced original text.\n' + ' - a "reason" key explaining why it is unreferenced.\n', + ], + stream=False, + ) + logger.debug(rsp) + json_blocks = parse_json_code_block(rsp) + if not json_blocks: + return [] + self._references = self.IntentDetectReferences.model_validate_json(json_blocks[0]) + + async def _get_sops(self): + intention_list = "" + for i, v in enumerate(self._references.intentions): + intention_list += f"{i + 1}. intent: {v.intent}\n" + for j in v.refs: + intention_list += f" - ref: {j}\n" + sop_list = "" + for i, v in enumerate(SOP_CONFIG): + sop_list += f"{i + 1}. {v.description}\n" + prompt = f"## Intentions\n{intention_list}\n---\n## SOPs\n{sop_list}\n" + rsp = await self.llm.aask( + prompt, + system_msgs=[ + "You are a tool that matches user intentions with Standard Operating Procedures (SOPs).", + 'You search for matching SOPs under "SOPs" based on user intentions in "Intentions" and their related original descriptions.', + 'Inspect each intention in "Intentions".', + "Return a markdown JSON list of objects, where each object contains:\n" + '- an "intent" key containing the intention from the "Intentions" section;\n' + '- a "sop" key containing the SOP description from the "SOPs" section; filled with an empty string if no match.\n' + '- a "sop_index" key containing the int type index of SOP description from the "SOPs" section; filled with 0 if no match.\n' + '- a "reason" key explaining why it is matching/mismatching.\n', + ], + stream=False, + ) + logger.debug(rsp) + json_blocks = parse_json_code_block(rsp) + vv = json.loads(json_blocks[0]) + self._intent_to_sops = [self.IntentSOP.model_validate(i) for i in vv] + + async def _merge(self): + self.result = IntentDetectResult(clarifications=self._dialog_intentions.clarifications) + distinct = {} + # Consolidate intentions under the same SOP. + for i in self._intent_to_sops: + if i.sop_index == 0: # 1-based index + refs = self._get_intent_ref(i.intent) + item = IntentDetectIntentionSOP(intention=IntentDetectIntentionRef(intent=i.intent, refs=refs)) + self.result.intentions.append(item) + continue + distinct[i.sop_index] = [i.intent] + distinct.get(i.sop_index, []) + + merge_intents = {} + intent_to_sops = {i.intent: i.sop_index for i in self._intent_to_sops if i.sop_index != 0} + for sop_index, intents in distinct.items(): + if len(intents) > 1: + merge_intents[sop_index] = intents + continue + # Merge single intention + refs = self._get_intent_ref(intents[0]) + item = IntentDetectIntentionSOP(intention=IntentDetectIntentionRef(intent=intents[0], refs=refs)) + sop_index = intent_to_sops.get(intents[0]) + item.sop = SOP_CONFIG[sop_index - 1] # 1-based index + self.result.intentions.append(item) + + # Merge repetitive intentions into one + for sop_index, intents in merge_intents.items(): + intent_ref = IntentDetectIntentionRef(intent="\n".join(intents), refs=[]) + for i in intents: + refs = self._get_intent_ref(i) + intent_ref.refs.extend(refs) + intent_ref.refs = list(set(intent_ref.refs)) + item = IntentDetectIntentionSOP(intention=intent_ref) + item.sop = SOP_CONFIG[sop_index - 1] # 1-based index + self.result.intentions.append(item) + + def _get_intent_ref(self, intent: str) -> List[str]: + refs = [] + for i in self._references.intentions: + if i.intent == intent: + refs.extend(i.refs) + return refs + + @staticmethod + def _message_to_markdown(messages) -> str: + markdown = "" + for i in messages: + content = i.content.replace("\n", " ") + markdown += f"> {i.role}: {content}\n>\n" + return markdown + + +class LightIntentDetect(IntentDetect): + async def run(self, with_messages: List[Message] = None, **kwargs) -> Message: + """ + Runs the intention detection action. + + Args: + with_messages (List[Message]): List of messages representing the conversation content. + **kwargs: Additional keyword arguments. + """ + msg_markdown = self._message_to_markdown(with_messages) + await self._get_intentions(msg_markdown) + await self._get_sops() + await self._merge() + + return Message(content="", role="assistant", cause_by=self) + + async def _get_sops(self): + intention_list = "" + for i, v in enumerate(self._dialog_intentions.intentions): + intention_list += f"{i + 1}. intent: {v.intent}\n - ref: {v.ref}\n" + sop_list = "" + for i, v in enumerate(SOP_CONFIG): + sop_list += f"{i + 1}. {v.description}\n" + prompt = f"## Intentions\n{intention_list}\n---\n## SOPs\n{sop_list}\n" + rsp = await self.llm.aask( + prompt, + system_msgs=[ + "You are a tool that matches user intentions with Standard Operating Procedures (SOPs).", + 'You search for matching SOPs under "SOPs" based on user intentions in "Intentions" and their related original descriptions.', + 'Inspect each intention in "Intentions".', + "Return a markdown JSON list of objects, where each object contains:\n" + '- an "intent" key containing the intention from the "Intentions" section;\n' + '- a "sop" key containing the SOP description from the "SOPs" section; filled with an empty string if no match.\n' + '- a "sop_index" key containing the int type index of SOP description from the "SOPs" section; filled with 0 if no match.\n' + '- a "reason" key explaining why it is matching/mismatching.\n', + ], + stream=False, + ) + logger.debug(rsp) + json_blocks = parse_json_code_block(rsp) + vv = json.loads(json_blocks[0]) + self._intent_to_sops = [self.IntentSOP.model_validate(i) for i in vv] + + async def _merge(self): + self.result = IntentDetectResult(clarifications=[]) + distinct = {} + # Consolidate intentions under the same SOP. + for i in self._intent_to_sops: + if i.sop_index == 0: # 1-based index + ref = self._get_intent_ref(i.intent) + item = IntentDetectIntentionSOP(intention=IntentDetectIntentionRef(intent=i.intent, refs=[ref])) + self.result.intentions.append(item) + continue + distinct[i.sop_index] = [i.intent] + distinct.get(i.sop_index, []) + + merge_intents = {} + intent_to_sops = {i.intent: i.sop_index for i in self._intent_to_sops if i.sop_index != 0} + for sop_index, intents in distinct.items(): + if len(intents) > 1: + merge_intents[sop_index] = intents + continue + # Merge single intention + ref = self._get_intent_ref(intents[0]) + item = IntentDetectIntentionSOP(intention=IntentDetectIntentionRef(intent=intents[0], refs=[ref])) + sop_index = intent_to_sops.get(intents[0]) # 1-based + if sop_index: + item.sop = SOP_CONFIG[sop_index - 1] # 1-based index + self.result.intentions.append(item) + + # Merge repetitive intentions into one + for sop_index, intents in merge_intents.items(): + intent_ref = IntentDetectIntentionRef(intent="\n".join(intents), refs=[]) + for i in intents: + ref = self._get_intent_ref(i) + intent_ref.refs.append(ref) + intent_ref.refs = list(set(intent_ref.refs)) + item = IntentDetectIntentionSOP(intention=intent_ref) + item.sop = SOP_CONFIG[sop_index - 1] # 1-based index + self.result.intentions.append(item) + + def _get_intent_ref(self, intent: str) -> str: + refs = [] + for i in self._dialog_intentions.intentions: + if i.intent == intent: + refs.append(i.ref) + return "\n".join(refs) diff --git a/tests/metagpt/actions/test_intent_detect.py b/tests/metagpt/actions/test_intent_detect.py new file mode 100644 index 0000000000..d200806731 --- /dev/null +++ b/tests/metagpt/actions/test_intent_detect.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import json + +import pytest + +from metagpt.actions.intent_detect import IntentDetect, LightIntentDetect +from metagpt.logs import logger +from metagpt.schema import Message + +DEMO_CONTENT = [ + { + "role": "user", + "content": "Can you build TextToSummarize which is a SMS number that I can text and it will scrape a website " + "and summarize it with ChatGPT", + }, + { + "role": "assistant", + "content": "Absolutely, I can build a service like TextToSummarize for you. The process will involve setting up" + " an SMS service that can receive your texts, scraping the website content you send, and then using" + " an AI to summarize the content.\nTo get started, I'll need to set up a number for receiving SMS" + ", then I'll work on the script to scrape the website content from the URLs you text, and finally." + " I'll integrate it with an AI service to Generalte the summaries.\n I'll keep you updated on my" + " progress!", + }, + {"role": "user", "content": "What exactly do we need the web app for?"}, + { + "role": "assistant", + "content": "The web app will service as the interface between the SMS service and the AI summarization service" + ". When you send a text with a URL to the SMS number, teh web app will process that, scrape the" + " content from the URL, and then use the AI to summarize it. The summary will then be sent back to" + " you. It's the central hub that ties all the components of the service togather.", + }, +] + +DEMO1_CONTENT = [ + { + "role": "user", + "content": "Extract all of the blog posts from `https://stripe.com/blog/page/1` and return a CSV with the" + " columns `date`, `article_text`, `author` and `summary`. Generate a summary for each article" + " yourself.", + } +] + +_DEMO2_CONTENT_ASSISTANT = """ +user: +## User Requirement +### User Requirement Detail +```text +2048 game +```` +### Knowledge +To meet user requirements, the following standard operating procedure(SOP) must be used. SOP descriptions cannot be modified; user requirements can only be appended to the end of corresponding steps. +- Writes a PRD based on software requirements. +- Writes a design to the project repository, based on the PRD of the project. +- Writes a project plan to the project repository, based on the design of the project. +- Writes code to implement designed features according to the project plan and adds them to the project repository. +- Stage and commit changes for the project repository using Git. +## Context + +## Current Plan +[ + { + "task_id": "1", + "dependent_task_ids": [], + "instruction": "Write a PRD based on the software requirements.", + "task_type": "other", + "code": "from metagpt.tools.libs.software_development import write_prd\n\nasync def create_2048_game_prd():\n idea = \"Create a 2048 game\"\n prd_path = await write_prd(idea)\n return prd_path\n\nawait create_2048_game_prd()\n", + "result": ",,,,,[,CONTENT]\n{\n \"Language\": \"en_us\",\n \"Programming Language\": \"Python\",\n \"Original Requirements\": \"Create a, 2048 game\",\n \"Project Name\": \"game_2048\",\n \"Product Goals\": [\n \"Create an engaging user experience\",\n , \"Improve accessibility, be responsive\",\n \"More beautiful UI\"\n ],\n \"User Stories\": [\n \",As a player, I want to be able to choose difficulty levels\",\n \"As a player,, I want to see my score after each game\",\n \"As a player, I want, to get restart button when I lose\",\n \"As a player, I want to see beautiful UI that make me feel good\",\n \"As a player, I want to play game via mobile phone\"\n , ],\n \"Competitive Analysis\": [\n \"2048 Game, A: Simple interface, lacks responsive features\",\n \"play2048.co: Beautiful and responsive UI with my best score shown\",\n \"2048game.com: Responsive UI with my, best score shown, but many ads\"\n ],\n \"Competitive Quadrant, Chart\": \"quadrantChart\\n title, \\\"Reach and engagement of campaigns\\\"\\n x-axis \\\"Low Reach\\\" --> \\\"High Reach\\\"\\n y-axis \\\"Low Engagement\\\" --> \\\",High Engagement\\\"\\n quadrant-1 \\\"We should expand\\\"\\n quadrant-2 \\\"Need to promote\\\"\\n quadrant,-3 \\\"Re-evaluate\\\"\\n quadrant-4 \\\"May be, improved\\\"\\n \\\"Campaign A\\\": [0.3, 0.6]\\n \\\"Campaign B\\\": [0.45, 0.,23]\\n \\\"Campaign C\\\": [0.57, 0.69]\\n \\\"Campaign D\\\": [0.78, 0.34]\\n , \\\"Campaign E\\\": [0.40, 0.34]\\n \\\"Campaign, F\\\": [0.35, 0.78]\\n \\\"Our Target Product\\\": [0.5, 0.6,]\",\n \"Requirement Analysis\": \"\",\n \"Requirement Pool\": [\n [\n \"P0\",\n, \"The main code ...\"\n ],\n [\n \"P0\",\n \"The game algorithm ...\"\n ]\n, ],\n \"UI Design draft\": \"Basic function description with a simple style and layout.\",\n \"Anything UNCLEAR,\": \"\"\n}\n[/CONTENT]\n,,,,,,,,,,,,{'output': '[{\"type_\": \"str\", \"name\": \"PRD File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/docs/prd/20240401194850.json\"}, {\"type_\": \"str\", \"name\": \"Competitive Analysis\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/competitive_analysis/20240401194850.mmd\"}, {\"type_\": \"str\", \"name\": \"Competitive Analysis\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/competitive_analysis/20240401194850.pdf\"}, {\"type_\": \"str\", \"name\": \"Competitive Analysis\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/competitive_analysis/20240401194850.png\"}, {\"type_\": \"str\", \"name\": \"Competitive Analysis\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/competitive_analysis/20240401194850.svg\"}]', 'tool_name': 'write_prd'},PosixPath('/Users/iorishinier/github/MetaGPT/workspace/game_2048/docs/prd')", + "is_success": true, + "is_finished": true + }, + { + "task_id": "2", + "dependent_task_ids": [ + "1" + ], + "instruction": "Write a design to the project repository, based on the PRD of the project.", + "task_type": "other", + "code": "from metagpt.tools.libs.software_development import write_design\n\nasync def create_2048_game_design(prd_path):\n system_design_path = await write_design(prd_path)\n return system_design_path\n\nawait create_2048_game_design('/Users/iorishinier/github/MetaGPT/workspace/game_2048/docs/prd/20240401194850.json')\n", + "result": ",[CONTENT]\n{\n \"Implementation approach\":, \"We will use the Py,game library to create the game interface and handle user input. The game logic will be implemented using Python classes and data structures to manage the game, board and tiles.\",\n \"File list\": [\n \"main.py,\",\n \"game.py\"\n, ],\n \"Data structures and interfaces\": \"\\nclassDiagram\\n class Game2048 {\\n -int[][] board,\\n -int score\\n -bool game_over\\n +__init__()\\n +reset()\\n +,move(direction: str)\\n +get_board() int[][]\\n +get_score() int\\n +is_game_over() bool\\n }\\n , class UI {\\n +display_board(board: int[][], score: int)\\n, +show_game_over()\\n }\\n Game,2048 --> UI\\n\",\n \"Program call flow\": \"\\nsequenceDiagram,\\n participant M as Main\\n participant G as Game2048,\\n participant UI as UI\\n M->>G: reset,()\\n G-->>UI: display_board(board, score)\\,n M->>G: move(direction)\\n G-->>,G: update board and score\\n G-->>UI:, display_board(board, score)\\n G->>G: is_game_over,()\\n G-->>UI: show_game_over()\\n\",\n,,,,, \"Anything UNCLEAR\": \"\"\n}\n[/CONTENT]\n,,,,,,,,,,,,,,,{'output': '[{\"type_\": \"str\", \"name\": \"Intermedia Design File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/docs/system_design/20240401194850.json\"}, {\"type_\": \"str\", \"name\": \"Design File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/system_design/20240401194850.md\"}, {\"type_\": \"str\", \"name\": \"Class Diagram File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/data_api_design/20240401194850.mmd\"}, {\"type_\": \"str\", \"name\": \"Class Diagram File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/data_api_design/20240401194850.pdf\"}, {\"type_\": \"str\", \"name\": \"Class Diagram File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/data_api_design/20240401194850.png\"}, {\"type_\": \"str\", \"name\": \"Class Diagram File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/data_api_design/20240401194850.svg\"}, {\"type_\": \"str\", \"name\": \"Sequence Diagram File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/seq_flow/20240401194850.mmd\"}, {\"type_\": \"str\", \"name\": \"Sequence Diagram File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/seq_flow/20240401194850.pdf\"}, {\"type_\": \"str\", \"name\": \"Sequence Diagram File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/seq_flow/20240401194850.png\"}, {\"type_\": \"str\", \"name\": \"Sequence Diagram File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/resources/seq_flow/20240401194850.svg\"}]', 'tool_name': 'write_design'},PosixPath('/Users/iorishinier/github/MetaGPT/workspace/game_2048/docs/system_design')", + "is_success": true, + "is_finished": true + }, + { + "task_id": "3", + "dependent_task_ids": [ + "2" + ], + "instruction": "Write a project plan to the project repository, based on the design of the project.", + "task_type": "other", + "code": "from metagpt.tools.libs.software_development import write_project_plan\n\nasync def create_2048_game_project_plan(system_design_path):\n task_path = await write_project_plan(system_design_path)\n return task_path\n\nawait create_2048_game_project_plan('/Users/iorishinier/github/MetaGPT/workspace/game_2048/docs/system_design/20240401194850.json')\n", + "result": ",\n[CONTENT,]\n{\n \"Required Python packages\": [\n \"pygame==2.0.1\"\n ],\n \"Required, Other language third-party packages\": [\n \"No third-party dependencies required,\"\n ],\n \"Logic Analysis\": [\n , [\n \"game.py\",\n \"Contains Game2048, class and functions to manage the game board and tiles\"\n, ],\n [\n \"main.py\",\n \",Contains the main function, and imports Game2048, from game.py\"\n ]\n ],\n \"Task list,\": [\n \"game.py\",\n \"main.py\"\n, ],\n \"Full API spec\":, \"\",\n \"Shared Knowledge\": \"The Pygame library will be used, to create the game interface and handle user input. The game, logic will be implemented using Python classes and data structures to manage the game, board and tiles.\",\n \"Anything UNCLEAR\": \"No unclear aspects,,,,, mentioned.\"\n}\n[/CONTENT]\n{'output': '[{\"type_\": \"str\", \"name\": \"Project Plan\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/docs/task/20240401194850.json\"}]', 'tool_name': 'write_project_plan'},PosixPath('/Users/iorishinier/github/MetaGPT/workspace/game_2048/docs/task')", + "is_success": true, + "is_finished": true + }, + { + "task_id": "4", + "dependent_task_ids": [ + "3" + ], + "instruction": "Write code to implement designed features according to the project plan and add them to the project repository.", + "task_type": "other", + "code": "from metagpt.tools.libs.software_development import write_codes\n\nasync def implement_2048_game_features(task_path):\n src_path = await write_codes(task_path)\n return src_path\n\nawait implement_2048_game_features('/Users/iorishinier/github/MetaGPT/workspace/game_2048/docs/task/20240401194850.json')\n", + "result": ",```python\n,## game.py\n\nclass Game2048,:\n def __init__(self):\n, self.board = [[0 for _, in range(4)] for _ in, range(4)]\n self.score =, 0\n self.game_over =, False\n\n def reset(self):\n , self.board = [[0 for _ in, range(4)] for _, in range(4)]\n self.score = 0\n self,.game_over = False\n\n , def move(self, direction: str):\n, # Implement the logic to move the, tiles in the specified direction\n, pass\n\n def get_board(self) ->, list:\n return self.board\n\n , def get_score(self) -> int:\n, return self.score\n\n def is,_game_over(self) -> bool:\n ,,,,, return self.game_over\n```\n,```,python\n## main.py\n\nimport pygame\nfrom game import Game2048\n\ndef main():\n # Initialize the game\n game = Game2048()\n game.reset()\n\n # Initialize Pygame\n, pygame.init()\n screen = pygame.display.set_mode((400, 400))\n pygame.display.set_caption('2048 Game,')\n\n running = True\n while running:\n for event in pygame.event.get():\n if, event.type == pygame.QUIT:\n running = False\n elif event.type == pygame.KEYDOWN:\n if event.key ==, pygame.K_UP:\n game.move('up')\n elif event.key == pygame.K_DOWN:\n , game.move('down')\n elif event.key == pygame.K_LEFT:\n game.move('left')\n elif, event.key == pygame.K_RIGHT,:\n game.move('right')\n\n # Display the game board and score\n board = game.get_board()\n score = game.get_score()\n display_board(screen, board, score)\n\n # Check, if the game is over\n if game.is_game_over():\n show_game_over(screen)\n\n pygame.display.update()\n\n pygame.quit()\n\n,def display_board(screen, board, score):\n # Write,,,, code to display the game board and score on the Pygame screen\n pass\n\ndef show_game_over(screen):\n # Write code to display the game over message on the Pygame screen\n pass\n\nif __name__ == \"__main__\":\n main()\n```\n,,{'output': '[{\"type_\": \"str\", \"name\": \"Source File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/game_2048/__init__.py\"}, {\"type_\": \"str\", \"name\": \"Source File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/game_2048/game.py\"}, {\"type_\": \"str\", \"name\": \"Source File\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048/game_2048/main.py\"}]', 'tool_name': 'write_codes'},PosixPath('/Users/iorishinier/github/MetaGPT/workspace/game_2048/game_2048')", + "is_success": true, + "is_finished": true + }, + { + "task_id": "5", + "dependent_task_ids": [ + "4" + ], + "instruction": "Stage and commit changes for the project repository using Git.", + "task_type": "other", + "code": "from metagpt.tools.libs.software_development import git_archive\n\nproject_path = '/Users/iorishinier/github/MetaGPT/workspace/game_2048'\ngit_log = await git_archive(project_path)\nprint(git_log)\n", + "result": ",{'output': '[{\"type_\": \"str\", \"name\": \"Git Commit\", \"value\": \"/Users/iorishinier/github/MetaGPT/workspace/game_2048\"}]', 'tool_name': 'git_archive'}commit 41c731341d0da8b630709bca50d4841dcea98d43\nAuthor: 莘权 马 \nDate: Mon Apr 1 19:50:53 2024 +0800\n\n Archive\n\ncommit a61daa93f24e74bf934ea5a3ba217038f167ab1f\nAuthor: 莘权 马 \nDate: Mon Apr 1 19:48:40 2024 +0800\n\n Add .gitignore\n", + "is_success": true, + "is_finished": true + } +] +## Current Task +{} + +""" + +DEMO2_CONTENT = [ + {"role": "user", "content": 'Create a "2048 game"'}, + {"role": "assistant", "content": _DEMO2_CONTENT_ASSISTANT}, + {"role": "user", "content": "TypeError: __init__() takes 1 positional argument but 2 were given"}, +] + +DEMO3_CONTENT = [ + {"role": "user", "content": "git clone 'https://github.com/spec-first/connexion' and format to MetaGPT project"} +] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "content", + [json.dumps(DEMO1_CONTENT), json.dumps(DEMO_CONTENT), json.dumps(DEMO2_CONTENT), json.dumps(DEMO3_CONTENT)], +) +async def test_intent_detect(content: str, context): + action = IntentDetect(context=context) + messages = [Message.model_validate(i) for i in json.loads(content)] + rsp = await action.run(messages) + assert isinstance(rsp, Message) + assert action._dialog_intentions + assert action._references + assert action._intent_to_sops + assert action.result + logger.info(action.result.model_dump_json()) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "content", + [json.dumps(DEMO1_CONTENT), json.dumps(DEMO_CONTENT), json.dumps(DEMO2_CONTENT), json.dumps(DEMO3_CONTENT)], +) +async def test_light_intent_detect(content: str, context): + action = LightIntentDetect(context=context) + messages = [Message.model_validate(i) for i in json.loads(content)] + rsp = await action.run(messages) + assert isinstance(rsp, Message) + assert action._dialog_intentions + assert action._intent_to_sops + assert action.result + + +if __name__ == "__main__": + pytest.main([__file__, "-s"])