From c1f05a91ca5ee1a6ecaafd10a81cdd68fc4462f7 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 26 Oct 2024 16:46:31 -0400 Subject: [PATCH 01/22] Prototyping critical systems thinking architecture. --- pyproject.toml | 6 +- src/goose_plugins/notes.md | 83 +++++++++ .../toolkits/critical_systems_thinking.py | 161 ++++++++++++++++++ .../prompts/critical_systems_thinking.jinja | 122 +++++++++++++ .../utils/selenium_web_browser.py | 53 ++++++ src/goose_plugins/utils/serper_search.py | 16 ++ .../test_critical_systems_thinking.py | 21 +++ 7 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 src/goose_plugins/notes.md create mode 100644 src/goose_plugins/toolkits/critical_systems_thinking.py create mode 100644 src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja create mode 100644 src/goose_plugins/utils/selenium_web_browser.py create mode 100644 src/goose_plugins/utils/serper_search.py create mode 100644 tests/toolkits/test_critical_systems_thinking.py diff --git a/pyproject.toml b/pyproject.toml index c509db7..246dac9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,10 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "ai-exchange>=0.8.4", - "goose-ai>=0.9.0", + "goose-ai>=0.9.8", + "requests>=2.32.3", + "selenium>=4.25.0", + "webdriver-manager>=4.0.2", ] author = [{ name = "Block", email = "ai-oss-tools@block.xyz" }] packages = [{ include = "goose_plugins", from = "src" }] @@ -19,6 +22,7 @@ goose-plugins = "goose_plugins:module_name" [project.entry-points."goose.toolkit"] artify = "goose_plugins.toolkits.artify:VincentVanCode" +critical_systems_thinking = "goose_plugins.toolkits.critical_systems_thinking:CriticalSystemsThinking" [build-system] diff --git a/src/goose_plugins/notes.md b/src/goose_plugins/notes.md new file mode 100644 index 0000000..1dbfc89 --- /dev/null +++ b/src/goose_plugins/notes.md @@ -0,0 +1,83 @@ +src/goose_plugins/toolkits/critical_thinker/ +**init**.py +critical_thinker.py +ethical_framework.py +context_analyzer.py +task_orchestrator.py +knowledge_manager.py +communication_handler.py +self_improvement.py + +2 Main Class (in critical_thinker.py): + +from goose.toolkit.base import Toolkit +from .ethical_framework import EthicalFramework +from .context_analyzer import ContextAnalyzer +from .task_orchestrator import TaskOrchestrator +from .knowledge_manager import KnowledgeManager +from .communication_handler import CommunicationHandler +from .self_improvement import SelfImprovement + +class CriticalThinker(Toolkit): +def **init**(self, *args, \*\*kwargs): +super().**init**(*args, \*\*kwargs) +self.ethical_framework = EthicalFramework() +self.context_analyzer = ContextAnalyzer() +self.task_orchestrator = TaskOrchestrator() +self.knowledge_manager = KnowledgeManager() +self.communication_handler = CommunicationHandler() +self.self_improvement = SelfImprovement() + + def analyze(self, problem_statement): + # Implement the main analysis logic here + pass + + def synthesize(self, analysis_results): + # Implement the synthesis of analysis results + pass + + def propose_solution(self, synthesis): + # Generate and evaluate potential solutions + pass + + def reflect(self): + # Perform self-reflection and improvement + pass + + # Implement other necessary methods + +3 Prompt Templates: Update the existing prompts or add new ones in: + +src/goose_plugins/toolkits/prompts/ +critical_thinker.jinja (update existing) +ethical_framework.jinja (new) +context_analyzer.jinja (new) +task_orchestrator.jinja (new) +knowledge_manager.jinja (new) +communication_handler.jinja (new) +self_improvement.jinja (new) + +4 Testing Structure: Update and add new tests in: + +tests/toolkits/ +test_critical_thinker.py (update existing) +test_ethical_framework.py (new) +test_context_analyzer.py (new) +test_task_orchestrator.py (new) +test_knowledge_manager.py (new) +test_communication_handler.py (new) +test_self_improvement.py (new) + +5 Implementation Steps: +6 Create the new directory structure under src/goose_plugins/toolkits/critical_thinker/. +7 Implement each component (ethical_framework.py, context_analyzer.py, etc.) with clear, well-documented interfaces. +8 Update the main CriticalThinker class in critical_thinker.py to orchestrate the components effectively. +9 Create or update the prompt templates in src/goose_plugins/toolkits/prompts/. +10 Develop comprehensive tests for each component and the system as a whole in the tests/toolkits/ directory. +11 Update the README.md in the goose-plugins repo to include information about the new CriticalThinker toolkit and how to use it. + +This revised design fits well within the existing structure of the goose-plugins repo. It maintains the modular approach, allowing +for flexibility and easy extension, while integrating seamlessly with the Goose ecosystem. + +To implement this change, we should start by creating the new directory structure and files. Would you like me to proceed with +creating the basic structure and skeleton code for the main CriticalThinker class? diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py new file mode 100644 index 0000000..9bd6e0a --- /dev/null +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -0,0 +1,161 @@ +from exchange import Exchange, Message, Text +from exchange.content import Content +from exchange.providers import AnthropicProvider +from goose.toolkit.base import Toolkit, tool +from goose.utils.ask import ask_an_ai +from goose_plugins.utils.selenium_web_browser import get_web_page_content +from goose_plugins.utils.serper_search import serper_search + + +class CriticalSystemsThinking(Toolkit): + """Critical Systems Thinking Toolkit for building and managing complex problems and systems.""" + + def message_content(self, content: Content) -> Text: + if isinstance(content, Text): + return content + else: + return Text(str(content)) + + @tool + def check_status(self) -> str: + """ + Check the status of the toolkit. Returns only "status: ok" if everything is working fine. + + Args: + statement (str): Additional context or information to check status. + + Returns: + response (str): A single line response indicating the status of the toolkit. + """ + # Create an instance of Exchange with the inlined OpenAI provider + self.notifier.status("checking status...") + + return "OK" + + @tool + def search(self, query: str) -> str: + """ + Search the web for information using the Serper API. This will return a list of search results. + + Args: + query (str): query to search for. + + Returns: + response (str): A JSON response containing search results. + """ + # Create an instance of Exchange with the inlined OpenAI provider + self.notifier.status("searching...") + + return serper_search(query) + + @tool + def analyze_request(self, statement: str) -> str: + """ + When a request is unclear, high-level or ambiguous use this tool to + analyze the response and provide a well thought out response. You should + return a well thought out response to the statement or question. + + Args: + statement (str): description of problem or errors seen. + + Returns: + response (str): A well thought out response to the statement or question. + """ + # Notify the user that the toolkit is analyzing the request + self.notifier.status("analyzing request...") + + # Create an instance of Exchange with the inlined OpenAI provider + provider = AnthropicProvider.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="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None) + request_input = f""" + Analyze the user statement: {statement} + If you need to immediately clarify something and it's something short and simple, respond with your question(s). + If you need multiple questions, you can ask multiple questions. + Please bullet point your questions. + Limit your response to 5 questions. + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) + return response.content[0].text + + @tool + def review_web_page(self, url: str) -> str: + """ + Review the content of a web page by providing a summary of the content. + + Args: + url (str): URL of the web page to review. + + Returns: + response (str): A summary of the content of the web page. + """ + # Notify the user that the toolkit is analyzing the request + self.notifier.status(f"fetching content from {url}") + + # Get the full HTML content of the web page + web_content = "" + try: + web_content = get_web_page_content(url) + except Exception as e: + return f"Error: {str(e)}" + + self.notifier.status(f"reviewing content: {web_content[:50]}...") + + provider = AnthropicProvider.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="claude-3-5-sonnet-20240620", messages=[], system=None) + request_input = f""" + summarize the following content: {web_content} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) + return response.content[0].text + + @tool + def consider_solutions(self, statement: str) -> str: + """ + Provide a well thought out response to the statement summarize the + problem and provide a solution or a set of solutions. + + Args: + statement (str): description of problem or errors seen. + + Returns: + response (str): A well thought out response to the statement or question. + """ + # Notify the user that the toolkit is analyzing the request + self.notifier.status("considering solutions...") + + # Create an instance of Exchange with the inlined OpenAI provider + provider = AnthropicProvider.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="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None) + request_input = f""" + Analyze the user statement: {statement} + Consider the existing message history and provide a well thought out response. + Provide one or more potential solutions to the problem. + Limit your response to 5 solutions. + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) + return response.content[0].text + + def system_prompt(self) -> str: + """Retrieve instructions on how to use this reasoning tool.""" + return Message.load("prompts/critical_systems_thinking.jinja").text diff --git a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja new file mode 100644 index 0000000..b5c7e74 --- /dev/null +++ b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja @@ -0,0 +1,122 @@ +# Operating Principles + +Respond in three tiers based on query complexity: +1. Simple (1-2 sentences) → Direct, precise response +2. Technical/practical → Structured analysis with explicit reasoning +3. Complex systems → Full critical systems analysis +4. Tool required for response → Use appropriate tool in toolkit + +# Technical & Practical Reasoning Framework +When addressing technical decisions or practical problems: + +1. Context Assessment +- Identify technical constraints and requirements +- Surface implicit assumptions +- Map dependencies and interfaces +- Note reliability/safety considerations + +2. Solution Space Analysis +- Generate multiple viable approaches +- Compare tradeoffs explicitly: + - Performance characteristics + - Implementation complexity + - Maintenance requirements + - Risk factors +- Test assumptions with "what if" scenarios + +3. Real-World Considerations +- Resource constraints (time, cost, skills) +- Operational impacts +- Future flexibility needs +- Common failure modes + +# Critical Systems Analysis Process +For complex system-level problems: + +1. Initial Rapid Assessment +- Core components + key interactions +- Immediate constraints + dynamics +- Critical assumptions to challenge + +2. Deep Systems Analysis +- Hidden variables + feedback loops +- Second/third-order effects +- System leverage points +- Emergent behaviors +- Stability characteristics + +3. Solution Development +- Multiple intervention points +- Phased implementation paths +- Success metrics +- Risk mitigation strategies + +# Response Structure +Frame analysis as: +Problem → Constraints → Options → Tradeoffs → Recommended Path + +Support with: +- Explicit "because" reasoning +- Concrete examples +- Edge case consideration +- Implementation guidance + +# Communication Principles +- Mirror human's technical depth while maintaining clarity +- Acknowledge uncertainty explicitly +- Seek clarification on ambiguous requirements +- Surface hidden complexities respectfully +- Provide actionable next steps + +# Expert Domain Behavior +When operating in technical domains: +- Apply domain-specific best practices +- Reference relevant patterns/anti-patterns +- Consider maintenance/operations impacts +- Flag security/reliability concerns +- Note compliance requirements +- Suggest monitoring/validation approaches + +# Common Sense Guidelines +When addressing practical problems: +- Consider human factors and usability +- Account for resource constraints +- Anticipate likely failure modes +- Suggest simple solutions first +- Provide fallback options +- Consider deployment context + +# Modes +- Standard: Apply appropriate analysis framework +- Testing: Reserved for specific test commands + - "ping" → "pong" + +# Tools + +Respond to basic questions as accurately and concisely as possible. + +Only return the content of a tool response with no additional commentary. + +If a request requires a tool to respond, use the appropriate tool your toolkit. + +If a request requires a tool to respond, in your response before invoking the tool only say "Using tool: [tool_name]". + +If you are asked about your status use the "check_status" tool defined in your toolkit. + +If you are asked a question you can answer without a tool and in a short response, do not use a tool. + +If you do not have a tool to respond to a query, describe the type of tool you would need to be able to respond to the query. + +If you are asked about something that requires realtime information about the world use the "serper_search" tool defined in your toolkit. + +If you are asked a request that is unclear, very broad, or not specific enough, use the "analyze_request" tool defined in your toolkit. + +If you think there may be a misunderstanding or typo in the request, use the "analyze_request" tool to clarify the request. + +If a user provides you with answers to your questions, but does not sufficiently answer all of your questions, use the "analyze_request" tool defined in your toolkit. + +If the user answers all of your questions sufficiently to move forward with a response, use the "consider_solutions" tool defined in your toolkit. + +If the user asks you to review a web page and gives you a url use the "review_web_page" tool defined in your toolkit. + +You should pass a single url to the review web page tool. If you are given multiple urls, you should ask the user to provide a single url. \ No newline at end of file diff --git a/src/goose_plugins/utils/selenium_web_browser.py b/src/goose_plugins/utils/selenium_web_browser.py new file mode 100644 index 0000000..4aa9014 --- /dev/null +++ b/src/goose_plugins/utils/selenium_web_browser.py @@ -0,0 +1,53 @@ +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By +from webdriver_manager.chrome import ChromeDriverManager +import time + +def chrome_driver_path(): + return "/opt/homebrew/bin/chromedriver" + +def get_web_page_content(url, wait_time=10): + # Set up Chrome options + chrome_options = Options() + chrome_options.add_argument("--headless") # Run in headless mode (no GUI) + chrome_options.add_argument("--disable-gpu") + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + + # Set up the Chrome driver + service = Service(ChromeDriverManager().install()) + driver = webdriver.Chrome(service=service, options=chrome_options) + + try: + # Navigate to the URL + driver.get(url) + + # Wait for the page to load + WebDriverWait(driver, wait_time).until( + EC.presence_of_element_located((By.TAG_NAME, "body")) + ) + + # Allow some time for JavaScript to execute + time.sleep(wait_time) + + # Get the page source + # page_source = driver.page_source + + # Get the text content + text_content = driver.find_element(By.TAG_NAME, "body").text + + return text_content + + finally: + # Close the browser + driver.quit() + +if __name__ == "__main__": + url = "https://www.google.com" + content = get_web_page_content(url) + print(content) + print("...") \ No newline at end of file diff --git a/src/goose_plugins/utils/serper_search.py b/src/goose_plugins/utils/serper_search.py new file mode 100644 index 0000000..e100db8 --- /dev/null +++ b/src/goose_plugins/utils/serper_search.py @@ -0,0 +1,16 @@ +import os +import json +import requests + +def serper_search(query): + payload = json.dumps({ 'q': query }) + headers = { + 'X-API-KEY': os.getenv('SERPER_API_KEY'), + 'Content-Type': 'application/json' + } + response = requests.request( + 'POST', + 'https://google.serper.dev/search', + headers=headers, data=payload + ) + return response.text \ No newline at end of file diff --git a/tests/toolkits/test_critical_systems_thinking.py b/tests/toolkits/test_critical_systems_thinking.py new file mode 100644 index 0000000..361a93e --- /dev/null +++ b/tests/toolkits/test_critical_systems_thinking.py @@ -0,0 +1,21 @@ +import pytest +from goose_plugins.toolkits.critical_systems_thinking import CriticalSystemsThinking +from unittest.mock import patch, MagicMock, Mock + + +@pytest.fixture +def critical_systems_thinking(): + notifier = MagicMock() + toolkit = CriticalSystemsThinking(notifier=notifier) + toolkit.exchange_view = Mock() + toolkit.exchange_view.processor = Mock() + toolkit.exchange_view.processor.messages = [] + return toolkit + +def test_check_status(critical_systems_thinking) -> None: + result = critical_systems_thinking.check_status() + + assert "OK" == result + +if __name__ == "__main__": + pytest.main() \ No newline at end of file From 4ec0f4be3def8774d62162f0c634a240773abc72 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 26 Oct 2024 16:52:32 -0400 Subject: [PATCH 02/22] Remove temporary notes. --- src/goose_plugins/notes.md | 83 -------------------------------------- 1 file changed, 83 deletions(-) delete mode 100644 src/goose_plugins/notes.md diff --git a/src/goose_plugins/notes.md b/src/goose_plugins/notes.md deleted file mode 100644 index 1dbfc89..0000000 --- a/src/goose_plugins/notes.md +++ /dev/null @@ -1,83 +0,0 @@ -src/goose_plugins/toolkits/critical_thinker/ -**init**.py -critical_thinker.py -ethical_framework.py -context_analyzer.py -task_orchestrator.py -knowledge_manager.py -communication_handler.py -self_improvement.py - -2 Main Class (in critical_thinker.py): - -from goose.toolkit.base import Toolkit -from .ethical_framework import EthicalFramework -from .context_analyzer import ContextAnalyzer -from .task_orchestrator import TaskOrchestrator -from .knowledge_manager import KnowledgeManager -from .communication_handler import CommunicationHandler -from .self_improvement import SelfImprovement - -class CriticalThinker(Toolkit): -def **init**(self, *args, \*\*kwargs): -super().**init**(*args, \*\*kwargs) -self.ethical_framework = EthicalFramework() -self.context_analyzer = ContextAnalyzer() -self.task_orchestrator = TaskOrchestrator() -self.knowledge_manager = KnowledgeManager() -self.communication_handler = CommunicationHandler() -self.self_improvement = SelfImprovement() - - def analyze(self, problem_statement): - # Implement the main analysis logic here - pass - - def synthesize(self, analysis_results): - # Implement the synthesis of analysis results - pass - - def propose_solution(self, synthesis): - # Generate and evaluate potential solutions - pass - - def reflect(self): - # Perform self-reflection and improvement - pass - - # Implement other necessary methods - -3 Prompt Templates: Update the existing prompts or add new ones in: - -src/goose_plugins/toolkits/prompts/ -critical_thinker.jinja (update existing) -ethical_framework.jinja (new) -context_analyzer.jinja (new) -task_orchestrator.jinja (new) -knowledge_manager.jinja (new) -communication_handler.jinja (new) -self_improvement.jinja (new) - -4 Testing Structure: Update and add new tests in: - -tests/toolkits/ -test_critical_thinker.py (update existing) -test_ethical_framework.py (new) -test_context_analyzer.py (new) -test_task_orchestrator.py (new) -test_knowledge_manager.py (new) -test_communication_handler.py (new) -test_self_improvement.py (new) - -5 Implementation Steps: -6 Create the new directory structure under src/goose_plugins/toolkits/critical_thinker/. -7 Implement each component (ethical_framework.py, context_analyzer.py, etc.) with clear, well-documented interfaces. -8 Update the main CriticalThinker class in critical_thinker.py to orchestrate the components effectively. -9 Create or update the prompt templates in src/goose_plugins/toolkits/prompts/. -10 Develop comprehensive tests for each component and the system as a whole in the tests/toolkits/ directory. -11 Update the README.md in the goose-plugins repo to include information about the new CriticalThinker toolkit and how to use it. - -This revised design fits well within the existing structure of the goose-plugins repo. It maintains the modular approach, allowing -for flexibility and easy extension, while integrating seamlessly with the Goose ecosystem. - -To implement this change, we should start by creating the new directory structure and files. Would you like me to proceed with -creating the basic structure and skeleton code for the main CriticalThinker class? From aac7a70fb3598e343ab7e73c9da7f018f022d0be Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 26 Oct 2024 16:54:27 -0400 Subject: [PATCH 03/22] Remove redundant comments. --- .../toolkits/critical_systems_thinking.py | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index 9bd6e0a..82f2fd3 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -27,7 +27,6 @@ def check_status(self) -> str: Returns: response (str): A single line response indicating the status of the toolkit. """ - # Create an instance of Exchange with the inlined OpenAI provider self.notifier.status("checking status...") return "OK" @@ -43,7 +42,6 @@ def search(self, query: str) -> str: Returns: response (str): A JSON response containing search results. """ - # Create an instance of Exchange with the inlined OpenAI provider self.notifier.status("searching...") return serper_search(query) @@ -61,13 +59,11 @@ def analyze_request(self, statement: str) -> str: Returns: response (str): A well thought out response to the statement or question. """ - # Notify the user that the toolkit is analyzing the request + self.notifier.status("analyzing request...") - # Create an instance of Exchange with the inlined OpenAI provider provider = AnthropicProvider.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 @@ -95,10 +91,10 @@ def review_web_page(self, url: str) -> str: Returns: response (str): A summary of the content of the web page. """ - # Notify the user that the toolkit is analyzing the request + self.notifier.status(f"fetching content from {url}") - # Get the full HTML content of the web page + # Get the text content of the web page web_content = "" try: web_content = get_web_page_content(url) @@ -109,12 +105,6 @@ def review_web_page(self, url: str) -> str: provider = AnthropicProvider.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="claude-3-5-sonnet-20240620", messages=[], system=None) request_input = f""" summarize the following content: {web_content} @@ -134,13 +124,11 @@ def consider_solutions(self, statement: str) -> str: Returns: response (str): A well thought out response to the statement or question. """ - # Notify the user that the toolkit is analyzing the request + self.notifier.status("considering solutions...") - # Create an instance of Exchange with the inlined OpenAI provider provider = AnthropicProvider.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 From 113a07d31482b0c03256455641b3e9f56ce0adc1 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 26 Oct 2024 16:56:09 -0400 Subject: [PATCH 04/22] Update doc string description. --- src/goose_plugins/toolkits/critical_systems_thinking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index 82f2fd3..0a3f4f7 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -8,7 +8,7 @@ class CriticalSystemsThinking(Toolkit): - """Critical Systems Thinking Toolkit for building and managing complex problems and systems.""" + """Critical systems thinking toolkit for understanding and solving complex problems.""" def message_content(self, content: Content) -> Text: if isinstance(content, Text): From e481ff3ce221b3c7351ebc72e5172c129fbb19fe Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 26 Oct 2024 17:02:23 -0400 Subject: [PATCH 05/22] Make linters happy. --- .../toolkits/critical_systems_thinking.py | 24 ++++++++++++++----- .../utils/selenium_web_browser.py | 12 +++++----- src/goose_plugins/utils/serper_search.py | 4 ++-- .../test_critical_systems_thinking.py | 8 +++---- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index 0a3f4f7..c65e4b9 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -15,7 +15,7 @@ def message_content(self, content: Content) -> Text: return content else: return Text(str(content)) - + @tool def check_status(self) -> str: """ @@ -49,7 +49,7 @@ def search(self, query: str) -> str: @tool def analyze_request(self, statement: str) -> str: """ - When a request is unclear, high-level or ambiguous use this tool to + When a request is unclear, high-level or ambiguous use this tool to analyze the response and provide a well thought out response. You should return a well thought out response to the statement or question. @@ -69,10 +69,16 @@ def analyze_request(self, statement: str) -> str: for msg in self.exchange_view.processor.messages ] - exchange = Exchange(provider=provider, model="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None) + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=existing_messages_copy, system=None + ) + request_input = f""" Analyze the user statement: {statement} - If you need to immediately clarify something and it's something short and simple, respond with your question(s). + If you need to immediately clarify something and it's something + short and simple, respond with your question(s). If you need multiple questions, you can ask multiple questions. Please bullet point your questions. Limit your response to 5 questions. @@ -115,7 +121,7 @@ def review_web_page(self, url: str) -> str: @tool def consider_solutions(self, statement: str) -> str: """ - Provide a well thought out response to the statement summarize the + Provide a well thought out response to the statement summarize the problem and provide a solution or a set of solutions. Args: @@ -134,7 +140,13 @@ def consider_solutions(self, statement: str) -> str: for msg in self.exchange_view.processor.messages ] - exchange = Exchange(provider=provider, model="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None) + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=existing_messages_copy, + system=None + ) + request_input = f""" Analyze the user statement: {statement} Consider the existing message history and provide a well thought out response. diff --git a/src/goose_plugins/utils/selenium_web_browser.py b/src/goose_plugins/utils/selenium_web_browser.py index 4aa9014..ad35d75 100644 --- a/src/goose_plugins/utils/selenium_web_browser.py +++ b/src/goose_plugins/utils/selenium_web_browser.py @@ -2,15 +2,15 @@ from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support import expected_conditions from selenium.webdriver.common.by import By from webdriver_manager.chrome import ChromeDriverManager import time -def chrome_driver_path(): +def chrome_driver_path() -> str: return "/opt/homebrew/bin/chromedriver" - -def get_web_page_content(url, wait_time=10): + +def get_web_page_content(url: str, wait_time: int=10) -> str: # Set up Chrome options chrome_options = Options() chrome_options.add_argument("--headless") # Run in headless mode (no GUI) @@ -28,7 +28,7 @@ def get_web_page_content(url, wait_time=10): # Wait for the page to load WebDriverWait(driver, wait_time).until( - EC.presence_of_element_located((By.TAG_NAME, "body")) + expected_conditions.presence_of_element_located((By.TAG_NAME, "body")) ) # Allow some time for JavaScript to execute @@ -50,4 +50,4 @@ def get_web_page_content(url, wait_time=10): url = "https://www.google.com" content = get_web_page_content(url) print(content) - print("...") \ No newline at end of file + print("...") diff --git a/src/goose_plugins/utils/serper_search.py b/src/goose_plugins/utils/serper_search.py index e100db8..165ba2d 100644 --- a/src/goose_plugins/utils/serper_search.py +++ b/src/goose_plugins/utils/serper_search.py @@ -2,7 +2,7 @@ import json import requests -def serper_search(query): +def serper_search(query: str) -> str: payload = json.dumps({ 'q': query }) headers = { 'X-API-KEY': os.getenv('SERPER_API_KEY'), @@ -13,4 +13,4 @@ def serper_search(query): 'https://google.serper.dev/search', headers=headers, data=payload ) - return response.text \ No newline at end of file + return response.text diff --git a/tests/toolkits/test_critical_systems_thinking.py b/tests/toolkits/test_critical_systems_thinking.py index 361a93e..579dc34 100644 --- a/tests/toolkits/test_critical_systems_thinking.py +++ b/tests/toolkits/test_critical_systems_thinking.py @@ -1,10 +1,10 @@ import pytest from goose_plugins.toolkits.critical_systems_thinking import CriticalSystemsThinking -from unittest.mock import patch, MagicMock, Mock +from unittest.mock import MagicMock, Mock @pytest.fixture -def critical_systems_thinking(): +def critical_systems_thinking() -> CriticalSystemsThinking: notifier = MagicMock() toolkit = CriticalSystemsThinking(notifier=notifier) toolkit.exchange_view = Mock() @@ -12,10 +12,10 @@ def critical_systems_thinking(): toolkit.exchange_view.processor.messages = [] return toolkit -def test_check_status(critical_systems_thinking) -> None: +def test_check_status(critical_systems_thinking: CriticalSystemsThinking) -> None: result = critical_systems_thinking.check_status() assert "OK" == result if __name__ == "__main__": - pytest.main() \ No newline at end of file + pytest.main() From 88f10e525992aeac52b0e44782e87357b62e49d1 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 26 Oct 2024 17:20:54 -0400 Subject: [PATCH 06/22] Make lints happy. --- .../toolkits/critical_systems_thinking.py | 9 ++------- src/goose_plugins/utils/selenium_web_browser.py | 9 +++++---- src/goose_plugins/utils/serper_search.py | 16 +++++----------- tests/toolkits/test_critical_systems_thinking.py | 2 ++ 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index c65e4b9..352d30b 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -70,9 +70,7 @@ def analyze_request(self, statement: str) -> str: ] exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=existing_messages_copy, system=None + provider=provider, model="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None ) request_input = f""" @@ -141,10 +139,7 @@ def consider_solutions(self, statement: str) -> str: ] exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=existing_messages_copy, - system=None + provider=provider, model="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None ) request_input = f""" diff --git a/src/goose_plugins/utils/selenium_web_browser.py b/src/goose_plugins/utils/selenium_web_browser.py index ad35d75..2a115e6 100644 --- a/src/goose_plugins/utils/selenium_web_browser.py +++ b/src/goose_plugins/utils/selenium_web_browser.py @@ -7,10 +7,12 @@ from webdriver_manager.chrome import ChromeDriverManager import time + def chrome_driver_path() -> str: return "/opt/homebrew/bin/chromedriver" -def get_web_page_content(url: str, wait_time: int=10) -> str: + +def get_web_page_content(url: str, wait_time: int = 10) -> str: # Set up Chrome options chrome_options = Options() chrome_options.add_argument("--headless") # Run in headless mode (no GUI) @@ -27,9 +29,7 @@ def get_web_page_content(url: str, wait_time: int=10) -> str: driver.get(url) # Wait for the page to load - WebDriverWait(driver, wait_time).until( - expected_conditions.presence_of_element_located((By.TAG_NAME, "body")) - ) + WebDriverWait(driver, wait_time).until(expected_conditions.presence_of_element_located((By.TAG_NAME, "body"))) # Allow some time for JavaScript to execute time.sleep(wait_time) @@ -46,6 +46,7 @@ def get_web_page_content(url: str, wait_time: int=10) -> str: # Close the browser driver.quit() + if __name__ == "__main__": url = "https://www.google.com" content = get_web_page_content(url) diff --git a/src/goose_plugins/utils/serper_search.py b/src/goose_plugins/utils/serper_search.py index 165ba2d..023836e 100644 --- a/src/goose_plugins/utils/serper_search.py +++ b/src/goose_plugins/utils/serper_search.py @@ -2,15 +2,9 @@ import json import requests + def serper_search(query: str) -> str: - payload = json.dumps({ 'q': query }) - headers = { - 'X-API-KEY': os.getenv('SERPER_API_KEY'), - 'Content-Type': 'application/json' - } - response = requests.request( - 'POST', - 'https://google.serper.dev/search', - headers=headers, data=payload - ) - return response.text + payload = json.dumps({"q": query}) + headers = {"X-API-KEY": os.getenv("SERPER_API_KEY"), "Content-Type": "application/json"} + response = requests.request("POST", "https://google.serper.dev/search", headers=headers, data=payload) + return response.text diff --git a/tests/toolkits/test_critical_systems_thinking.py b/tests/toolkits/test_critical_systems_thinking.py index 579dc34..8bd7935 100644 --- a/tests/toolkits/test_critical_systems_thinking.py +++ b/tests/toolkits/test_critical_systems_thinking.py @@ -12,10 +12,12 @@ def critical_systems_thinking() -> CriticalSystemsThinking: toolkit.exchange_view.processor.messages = [] return toolkit + def test_check_status(critical_systems_thinking: CriticalSystemsThinking) -> None: result = critical_systems_thinking.check_status() assert "OK" == result + if __name__ == "__main__": pytest.main() From 0297654c8808ac77bf09c6ad2d8d01986ba56329 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 26 Oct 2024 17:30:55 -0400 Subject: [PATCH 07/22] Maker linters happy. --- src/goose_plugins/toolkits/critical_systems_thinking.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index 352d30b..97ff9db 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -21,9 +21,6 @@ def check_status(self) -> str: """ Check the status of the toolkit. Returns only "status: ok" if everything is working fine. - Args: - statement (str): Additional context or information to check status. - Returns: response (str): A single line response indicating the status of the toolkit. """ From fab49d3d7a3ebeba013b231f736b42837fc2d4d2 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 26 Oct 2024 18:07:38 -0400 Subject: [PATCH 08/22] Flesh out tests. --- .../toolkits/critical_systems_thinking.py | 17 +- .../prompts/critical_systems_thinking.jinja | 23 ++- .../test_critical_systems_thinking.py | 151 +++++++++++++++++- 3 files changed, 158 insertions(+), 33 deletions(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index 97ff9db..c280786 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -16,18 +16,6 @@ def message_content(self, content: Content) -> Text: else: return Text(str(content)) - @tool - def check_status(self) -> str: - """ - Check the status of the toolkit. Returns only "status: ok" if everything is working fine. - - Returns: - response (str): A single line response indicating the status of the toolkit. - """ - self.notifier.status("checking status...") - - return "OK" - @tool def search(self, query: str) -> str: """ @@ -67,7 +55,10 @@ def analyze_request(self, statement: str) -> str: ] exchange = Exchange( - provider=provider, model="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=existing_messages_copy, + system=self.system_prompt() ) request_input = f""" diff --git a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja index b5c7e74..9e2adf6 100644 --- a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja +++ b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja @@ -1,5 +1,7 @@ # Operating Principles +# Response Tiers + Respond in three tiers based on query complexity: 1. Simple (1-2 sentences) → Direct, precise response 2. Technical/practical → Structured analysis with explicit reasoning @@ -86,22 +88,15 @@ When addressing practical problems: - Provide fallback options - Consider deployment context -# Modes -- Standard: Apply appropriate analysis framework -- Testing: Reserved for specific test commands - - "ping" → "pong" - # Tools -Respond to basic questions as accurately and concisely as possible. +If you are asked a basic question that can be without at tool, respond accurately and concisely. -Only return the content of a tool response with no additional commentary. +If you need to use a tool to respond to a request, return the tool response with no additional commentary. -If a request requires a tool to respond, use the appropriate tool your toolkit. +If you need to use a tool to respond to a request, use the appropriate tool your toolkit. -If a request requires a tool to respond, in your response before invoking the tool only say "Using tool: [tool_name]". - -If you are asked about your status use the "check_status" tool defined in your toolkit. +If you need to use a tool to respond to a request, in your response before invoking the tool only say "Using tool: [tool_name]". If you are asked a question you can answer without a tool and in a short response, do not use a tool. @@ -113,10 +108,12 @@ If you are asked a request that is unclear, very broad, or not specific enough, If you think there may be a misunderstanding or typo in the request, use the "analyze_request" tool to clarify the request. -If a user provides you with answers to your questions, but does not sufficiently answer all of your questions, use the "analyze_request" tool defined in your toolkit. +If you are provided with answers to your questions, but does not sufficiently answer all of your questions, use the "analyze_request" tool defined in your toolkit. If the user answers all of your questions sufficiently to move forward with a response, use the "consider_solutions" tool defined in your toolkit. If the user asks you to review a web page and gives you a url use the "review_web_page" tool defined in your toolkit. -You should pass a single url to the review web page tool. If you are given multiple urls, you should ask the user to provide a single url. \ No newline at end of file +If you invoke the review web page you should pass a single url to the tool. If you are given multiple urls, you should ask the user to provide a single url. + +If you are asked about something and given a url always use the review web page tool to respond. \ No newline at end of file diff --git a/tests/toolkits/test_critical_systems_thinking.py b/tests/toolkits/test_critical_systems_thinking.py index 8bd7935..0d32225 100644 --- a/tests/toolkits/test_critical_systems_thinking.py +++ b/tests/toolkits/test_critical_systems_thinking.py @@ -1,23 +1,160 @@ import pytest +from unittest.mock import MagicMock, Mock, patch +from exchange import Exchange, Text +from exchange.content import Content +from typing import Any from goose_plugins.toolkits.critical_systems_thinking import CriticalSystemsThinking -from unittest.mock import MagicMock, Mock @pytest.fixture def critical_systems_thinking() -> CriticalSystemsThinking: - notifier = MagicMock() - toolkit = CriticalSystemsThinking(notifier=notifier) + notifier: MagicMock = MagicMock() + toolkit: CriticalSystemsThinking = CriticalSystemsThinking(notifier=notifier) toolkit.exchange_view = Mock() toolkit.exchange_view.processor = Mock() toolkit.exchange_view.processor.messages = [] return toolkit -def test_check_status(critical_systems_thinking: CriticalSystemsThinking) -> None: - result = critical_systems_thinking.check_status() +def test_message_content_with_text(critical_systems_thinking: CriticalSystemsThinking) -> None: + text: Text = Text("test message") + result: Text = critical_systems_thinking.message_content(text) - assert "OK" == result + assert result == text + + +def test_message_content_with_other_content(critical_systems_thinking: CriticalSystemsThinking) -> None: + content: Mock = Mock(spec=Content) + content.__str__ = Mock(return_value="test content") + + result: Text = critical_systems_thinking.message_content(content) + + assert isinstance(result, Text) + assert result.text == "test content" + + +@patch('goose_plugins.toolkits.critical_systems_thinking.serper_search') +def test_search( + mock_serper_search: Mock, + critical_systems_thinking: CriticalSystemsThinking +) -> None: + expected_response: str = '{"results": []}' + mock_serper_search.return_value = expected_response + query: str = "test query" + + result: str = critical_systems_thinking.search(query) + + assert result == expected_response + mock_serper_search.assert_called_once_with(query) + critical_systems_thinking.notifier.status.assert_called_once_with("searching...") + + +@patch('goose_plugins.toolkits.critical_systems_thinking.AnthropicProvider') +def test_analyze_request( + mock_provider_class: Mock, + critical_systems_thinking: CriticalSystemsThinking +) -> None: + expected_analysis: str = "Analysis result" + mock_provider: Mock = Mock() + mock_provider_class.from_env.return_value = mock_provider + mock_response: Mock = Mock() + mock_response.content = [Text(expected_analysis)] + + with patch('goose_plugins.toolkits.critical_systems_thinking.ask_an_ai') as mock_ask: + mock_ask.return_value = mock_response + statement: str = "test statement" + + result: str = critical_systems_thinking.analyze_request(statement) + + assert result == expected_analysis + critical_systems_thinking.notifier.status.assert_called_with("analyzing request...") + + call_args: Any = mock_ask.call_args + assert statement in call_args[1]['input'] + assert isinstance(call_args[1]['exchange'], Exchange) + + +@patch('goose_plugins.toolkits.critical_systems_thinking.get_web_page_content') +@patch('goose_plugins.toolkits.critical_systems_thinking.AnthropicProvider') +def test_review_web_page( + mock_provider_class: Mock, + mock_get_content: Mock, + critical_systems_thinking: CriticalSystemsThinking +) -> None: + web_content: str = "Sample web content" + expected_summary: str = "Page summary" + url: str = "http://example.com" + + mock_get_content.return_value = web_content + mock_provider: Mock = Mock() + mock_provider_class.from_env.return_value = mock_provider + mock_response: Mock = Mock() + mock_response.content = [Text(expected_summary)] + + with patch('goose_plugins.toolkits.critical_systems_thinking.ask_an_ai') as mock_ask: + mock_ask.return_value = mock_response + + result: str = critical_systems_thinking.review_web_page(url) + + assert result == expected_summary + mock_get_content.assert_called_once_with(url) + critical_systems_thinking.notifier.status.assert_any_call(f"fetching content from {url}") + critical_systems_thinking.notifier.status.assert_any_call(f"reviewing content: {web_content[:50]}...") + + +@patch('goose_plugins.toolkits.critical_systems_thinking.get_web_page_content') +def test_review_web_page_error( + mock_get_content: Mock, + critical_systems_thinking: CriticalSystemsThinking +) -> None: + error_message: str = "Failed to fetch" + url: str = "http://example.com" + mock_get_content.side_effect = Exception(error_message) + + result: str = critical_systems_thinking.review_web_page(url) + + assert result == f"Error: {error_message}" + mock_get_content.assert_called_once_with(url) + + +@patch('goose_plugins.toolkits.critical_systems_thinking.AnthropicProvider') +def test_consider_solutions( + mock_provider_class: Mock, + critical_systems_thinking: CriticalSystemsThinking +) -> None: + expected_solution: str = "Solution analysis" + mock_provider: Mock = Mock() + mock_provider_class.from_env.return_value = mock_provider + mock_response: Mock = Mock() + mock_response.content = [Text(expected_solution)] + + with patch('goose_plugins.toolkits.critical_systems_thinking.ask_an_ai') as mock_ask: + mock_ask.return_value = mock_response + statement: str = "test problem" + + result: str = critical_systems_thinking.consider_solutions(statement) + + assert result == expected_solution + critical_systems_thinking.notifier.status.assert_called_with("considering solutions...") + + call_args: Any = mock_ask.call_args + assert statement in call_args[1]['input'] + assert isinstance(call_args[1]['exchange'], Exchange) + + +def test_system_prompt(critical_systems_thinking: CriticalSystemsThinking) -> None: + expected_prompt: str = "System prompt content" + + with patch('exchange.Message.load') as mock_load: + mock_message: Mock = Mock() + mock_message.text = expected_prompt + mock_load.return_value = mock_message + + result: str = critical_systems_thinking.system_prompt() + + assert result == expected_prompt + mock_load.assert_called_once_with("prompts/critical_systems_thinking.jinja") if __name__ == "__main__": - pytest.main() + pytest.main(["-v"]) From dde045f4e0532dc6c12faf0307469de19490bcf0 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 26 Oct 2024 18:22:26 -0400 Subject: [PATCH 09/22] Run ruff format. --- .../toolkits/critical_systems_thinking.py | 2 +- .../test_critical_systems_thinking.py | 52 +++++++------------ 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index c280786..cde7a56 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -58,7 +58,7 @@ def analyze_request(self, statement: str) -> str: provider=provider, model="claude-3-5-sonnet-20240620", messages=existing_messages_copy, - system=self.system_prompt() + system=self.system_prompt(), ) request_input = f""" diff --git a/tests/toolkits/test_critical_systems_thinking.py b/tests/toolkits/test_critical_systems_thinking.py index 0d32225..6e4e6b5 100644 --- a/tests/toolkits/test_critical_systems_thinking.py +++ b/tests/toolkits/test_critical_systems_thinking.py @@ -33,11 +33,8 @@ def test_message_content_with_other_content(critical_systems_thinking: CriticalS assert result.text == "test content" -@patch('goose_plugins.toolkits.critical_systems_thinking.serper_search') -def test_search( - mock_serper_search: Mock, - critical_systems_thinking: CriticalSystemsThinking -) -> None: +@patch("goose_plugins.toolkits.critical_systems_thinking.serper_search") +def test_search(mock_serper_search: Mock, critical_systems_thinking: CriticalSystemsThinking) -> None: expected_response: str = '{"results": []}' mock_serper_search.return_value = expected_response query: str = "test query" @@ -49,18 +46,15 @@ def test_search( critical_systems_thinking.notifier.status.assert_called_once_with("searching...") -@patch('goose_plugins.toolkits.critical_systems_thinking.AnthropicProvider') -def test_analyze_request( - mock_provider_class: Mock, - critical_systems_thinking: CriticalSystemsThinking -) -> None: +@patch("goose_plugins.toolkits.critical_systems_thinking.AnthropicProvider") +def test_analyze_request(mock_provider_class: Mock, critical_systems_thinking: CriticalSystemsThinking) -> None: expected_analysis: str = "Analysis result" mock_provider: Mock = Mock() mock_provider_class.from_env.return_value = mock_provider mock_response: Mock = Mock() mock_response.content = [Text(expected_analysis)] - with patch('goose_plugins.toolkits.critical_systems_thinking.ask_an_ai') as mock_ask: + with patch("goose_plugins.toolkits.critical_systems_thinking.ask_an_ai") as mock_ask: mock_ask.return_value = mock_response statement: str = "test statement" @@ -70,16 +64,14 @@ def test_analyze_request( critical_systems_thinking.notifier.status.assert_called_with("analyzing request...") call_args: Any = mock_ask.call_args - assert statement in call_args[1]['input'] - assert isinstance(call_args[1]['exchange'], Exchange) + assert statement in call_args[1]["input"] + assert isinstance(call_args[1]["exchange"], Exchange) -@patch('goose_plugins.toolkits.critical_systems_thinking.get_web_page_content') -@patch('goose_plugins.toolkits.critical_systems_thinking.AnthropicProvider') +@patch("goose_plugins.toolkits.critical_systems_thinking.get_web_page_content") +@patch("goose_plugins.toolkits.critical_systems_thinking.AnthropicProvider") def test_review_web_page( - mock_provider_class: Mock, - mock_get_content: Mock, - critical_systems_thinking: CriticalSystemsThinking + mock_provider_class: Mock, mock_get_content: Mock, critical_systems_thinking: CriticalSystemsThinking ) -> None: web_content: str = "Sample web content" expected_summary: str = "Page summary" @@ -91,7 +83,7 @@ def test_review_web_page( mock_response: Mock = Mock() mock_response.content = [Text(expected_summary)] - with patch('goose_plugins.toolkits.critical_systems_thinking.ask_an_ai') as mock_ask: + with patch("goose_plugins.toolkits.critical_systems_thinking.ask_an_ai") as mock_ask: mock_ask.return_value = mock_response result: str = critical_systems_thinking.review_web_page(url) @@ -102,11 +94,8 @@ def test_review_web_page( critical_systems_thinking.notifier.status.assert_any_call(f"reviewing content: {web_content[:50]}...") -@patch('goose_plugins.toolkits.critical_systems_thinking.get_web_page_content') -def test_review_web_page_error( - mock_get_content: Mock, - critical_systems_thinking: CriticalSystemsThinking -) -> None: +@patch("goose_plugins.toolkits.critical_systems_thinking.get_web_page_content") +def test_review_web_page_error(mock_get_content: Mock, critical_systems_thinking: CriticalSystemsThinking) -> None: error_message: str = "Failed to fetch" url: str = "http://example.com" mock_get_content.side_effect = Exception(error_message) @@ -117,18 +106,15 @@ def test_review_web_page_error( mock_get_content.assert_called_once_with(url) -@patch('goose_plugins.toolkits.critical_systems_thinking.AnthropicProvider') -def test_consider_solutions( - mock_provider_class: Mock, - critical_systems_thinking: CriticalSystemsThinking -) -> None: +@patch("goose_plugins.toolkits.critical_systems_thinking.AnthropicProvider") +def test_consider_solutions(mock_provider_class: Mock, critical_systems_thinking: CriticalSystemsThinking) -> None: expected_solution: str = "Solution analysis" mock_provider: Mock = Mock() mock_provider_class.from_env.return_value = mock_provider mock_response: Mock = Mock() mock_response.content = [Text(expected_solution)] - with patch('goose_plugins.toolkits.critical_systems_thinking.ask_an_ai') as mock_ask: + with patch("goose_plugins.toolkits.critical_systems_thinking.ask_an_ai") as mock_ask: mock_ask.return_value = mock_response statement: str = "test problem" @@ -138,14 +124,14 @@ def test_consider_solutions( critical_systems_thinking.notifier.status.assert_called_with("considering solutions...") call_args: Any = mock_ask.call_args - assert statement in call_args[1]['input'] - assert isinstance(call_args[1]['exchange'], Exchange) + assert statement in call_args[1]["input"] + assert isinstance(call_args[1]["exchange"], Exchange) def test_system_prompt(critical_systems_thinking: CriticalSystemsThinking) -> None: expected_prompt: str = "System prompt content" - with patch('exchange.Message.load') as mock_load: + with patch("exchange.Message.load") as mock_load: mock_message: Mock = Mock() mock_message.text = expected_prompt mock_load.return_value = mock_message From cd17886e2e6b7a019148da166fac052360b31514 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 00:49:12 -0400 Subject: [PATCH 10/22] WIP --- src/goose_plugins/toolkits/prompts/cst.jinja | 119 +++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/goose_plugins/toolkits/prompts/cst.jinja diff --git a/src/goose_plugins/toolkits/prompts/cst.jinja b/src/goose_plugins/toolkits/prompts/cst.jinja new file mode 100644 index 0000000..9e2adf6 --- /dev/null +++ b/src/goose_plugins/toolkits/prompts/cst.jinja @@ -0,0 +1,119 @@ +# Operating Principles + +# Response Tiers + +Respond in three tiers based on query complexity: +1. Simple (1-2 sentences) → Direct, precise response +2. Technical/practical → Structured analysis with explicit reasoning +3. Complex systems → Full critical systems analysis +4. Tool required for response → Use appropriate tool in toolkit + +# Technical & Practical Reasoning Framework +When addressing technical decisions or practical problems: + +1. Context Assessment +- Identify technical constraints and requirements +- Surface implicit assumptions +- Map dependencies and interfaces +- Note reliability/safety considerations + +2. Solution Space Analysis +- Generate multiple viable approaches +- Compare tradeoffs explicitly: + - Performance characteristics + - Implementation complexity + - Maintenance requirements + - Risk factors +- Test assumptions with "what if" scenarios + +3. Real-World Considerations +- Resource constraints (time, cost, skills) +- Operational impacts +- Future flexibility needs +- Common failure modes + +# Critical Systems Analysis Process +For complex system-level problems: + +1. Initial Rapid Assessment +- Core components + key interactions +- Immediate constraints + dynamics +- Critical assumptions to challenge + +2. Deep Systems Analysis +- Hidden variables + feedback loops +- Second/third-order effects +- System leverage points +- Emergent behaviors +- Stability characteristics + +3. Solution Development +- Multiple intervention points +- Phased implementation paths +- Success metrics +- Risk mitigation strategies + +# Response Structure +Frame analysis as: +Problem → Constraints → Options → Tradeoffs → Recommended Path + +Support with: +- Explicit "because" reasoning +- Concrete examples +- Edge case consideration +- Implementation guidance + +# Communication Principles +- Mirror human's technical depth while maintaining clarity +- Acknowledge uncertainty explicitly +- Seek clarification on ambiguous requirements +- Surface hidden complexities respectfully +- Provide actionable next steps + +# Expert Domain Behavior +When operating in technical domains: +- Apply domain-specific best practices +- Reference relevant patterns/anti-patterns +- Consider maintenance/operations impacts +- Flag security/reliability concerns +- Note compliance requirements +- Suggest monitoring/validation approaches + +# Common Sense Guidelines +When addressing practical problems: +- Consider human factors and usability +- Account for resource constraints +- Anticipate likely failure modes +- Suggest simple solutions first +- Provide fallback options +- Consider deployment context + +# Tools + +If you are asked a basic question that can be without at tool, respond accurately and concisely. + +If you need to use a tool to respond to a request, return the tool response with no additional commentary. + +If you need to use a tool to respond to a request, use the appropriate tool your toolkit. + +If you need to use a tool to respond to a request, in your response before invoking the tool only say "Using tool: [tool_name]". + +If you are asked a question you can answer without a tool and in a short response, do not use a tool. + +If you do not have a tool to respond to a query, describe the type of tool you would need to be able to respond to the query. + +If you are asked about something that requires realtime information about the world use the "serper_search" tool defined in your toolkit. + +If you are asked a request that is unclear, very broad, or not specific enough, use the "analyze_request" tool defined in your toolkit. + +If you think there may be a misunderstanding or typo in the request, use the "analyze_request" tool to clarify the request. + +If you are provided with answers to your questions, but does not sufficiently answer all of your questions, use the "analyze_request" tool defined in your toolkit. + +If the user answers all of your questions sufficiently to move forward with a response, use the "consider_solutions" tool defined in your toolkit. + +If the user asks you to review a web page and gives you a url use the "review_web_page" tool defined in your toolkit. + +If you invoke the review web page you should pass a single url to the tool. If you are given multiple urls, you should ask the user to provide a single url. + +If you are asked about something and given a url always use the review web page tool to respond. \ No newline at end of file From 3d8e73e9cd05520d30225faa3626972b7758ffd5 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 00:49:25 -0400 Subject: [PATCH 11/22] WIP --- .../prompts/critical_systems_thinking.jinja | 139 ++++-------------- 1 file changed, 30 insertions(+), 109 deletions(-) diff --git a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja index 9e2adf6..5b963fc 100644 --- a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja +++ b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja @@ -1,119 +1,40 @@ -# Operating Principles + You are an AI assistant specialized in critical systems thinking. Your role is to analyze complex problems and provid + comprehensive, anticipatory solutions using a systematic approach. Follow these steps when addressing a problem: -# Response Tiers + 1. Problem Analysis: + - Use the `analyze_request` tool to clarify the problem statement if needed. + - Employ the `search` tool to gather relevant information about the problem context. -Respond in three tiers based on query complexity: -1. Simple (1-2 sentences) → Direct, precise response -2. Technical/practical → Structured analysis with explicit reasoning -3. Complex systems → Full critical systems analysis -4. Tool required for response → Use appropriate tool in toolkit + 2. Stakeholder Identification: + - Use the `stakeholder_analysis` tool to identify key stakeholders, their interests, and potential impacts. -# Technical & Practical Reasoning Framework -When addressing technical decisions or practical problems: + 3. System Mapping: + - Apply the `system_mapping` tool to create a high-level map of the system, identifying key components and their + relationships. -1. Context Assessment -- Identify technical constraints and requirements -- Surface implicit assumptions -- Map dependencies and interfaces -- Note reliability/safety considerations + 4. Future Scenario Generation: + - Utilize the `generate_future_scenarios` tool to explore potential future outcomes and challenges. -2. Solution Space Analysis -- Generate multiple viable approaches -- Compare tradeoffs explicitly: - - Performance characteristics - - Implementation complexity - - Maintenance requirements - - Risk factors -- Test assumptions with "what if" scenarios + 5. Solution Consideration: + - Use the `consider_solutions` tool to generate potential solutions based on the comprehensive analysis. -3. Real-World Considerations -- Resource constraints (time, cost, skills) -- Operational impacts -- Future flexibility needs -- Common failure modes + 6. Risk Assessment: + - Apply the `risk_assessment` tool to evaluate potential risks associated with the proposed solutions. -# Critical Systems Analysis Process -For complex system-level problems: + 7. Web Research: + - If additional information is needed, use the `review_web_page` tool to analyze relevant web content. -1. Initial Rapid Assessment -- Core components + key interactions -- Immediate constraints + dynamics -- Critical assumptions to challenge + 8. Synthesis and Recommendation: + - Synthesize the insights gained from all previous steps. + - Provide a well-reasoned recommendation that addresses the problem comprehensively, considers future scenarios, a + mitigates potential risks. -2. Deep Systems Analysis -- Hidden variables + feedback loops -- Second/third-order effects -- System leverage points -- Emergent behaviors -- Stability characteristics + Remember to: + - Consider long-term consequences and systemic impacts of proposed solutions. + - Identify and analyze feedback loops within the system. + - Address the interconnectedness of various components and stakeholders. + - Anticipate potential unintended consequences of interventions. + - Propose adaptive and flexible solutions that can respond to changing conditions. -3. Solution Development -- Multiple intervention points -- Phased implementation paths -- Success metrics -- Risk mitigation strategies - -# Response Structure -Frame analysis as: -Problem → Constraints → Options → Tradeoffs → Recommended Path - -Support with: -- Explicit "because" reasoning -- Concrete examples -- Edge case consideration -- Implementation guidance - -# Communication Principles -- Mirror human's technical depth while maintaining clarity -- Acknowledge uncertainty explicitly -- Seek clarification on ambiguous requirements -- Surface hidden complexities respectfully -- Provide actionable next steps - -# Expert Domain Behavior -When operating in technical domains: -- Apply domain-specific best practices -- Reference relevant patterns/anti-patterns -- Consider maintenance/operations impacts -- Flag security/reliability concerns -- Note compliance requirements -- Suggest monitoring/validation approaches - -# Common Sense Guidelines -When addressing practical problems: -- Consider human factors and usability -- Account for resource constraints -- Anticipate likely failure modes -- Suggest simple solutions first -- Provide fallback options -- Consider deployment context - -# Tools - -If you are asked a basic question that can be without at tool, respond accurately and concisely. - -If you need to use a tool to respond to a request, return the tool response with no additional commentary. - -If you need to use a tool to respond to a request, use the appropriate tool your toolkit. - -If you need to use a tool to respond to a request, in your response before invoking the tool only say "Using tool: [tool_name]". - -If you are asked a question you can answer without a tool and in a short response, do not use a tool. - -If you do not have a tool to respond to a query, describe the type of tool you would need to be able to respond to the query. - -If you are asked about something that requires realtime information about the world use the "serper_search" tool defined in your toolkit. - -If you are asked a request that is unclear, very broad, or not specific enough, use the "analyze_request" tool defined in your toolkit. - -If you think there may be a misunderstanding or typo in the request, use the "analyze_request" tool to clarify the request. - -If you are provided with answers to your questions, but does not sufficiently answer all of your questions, use the "analyze_request" tool defined in your toolkit. - -If the user answers all of your questions sufficiently to move forward with a response, use the "consider_solutions" tool defined in your toolkit. - -If the user asks you to review a web page and gives you a url use the "review_web_page" tool defined in your toolkit. - -If you invoke the review web page you should pass a single url to the tool. If you are given multiple urls, you should ask the user to provide a single url. - -If you are asked about something and given a url always use the review web page tool to respond. \ No newline at end of file + Your goal is to provide a holistic, forward-thinking analysis and solution that addresses the complex nature of the + problem at hand. \ No newline at end of file From 49e02b03da16a0682bb1a139e52a76052c726626 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 01:45:04 -0400 Subject: [PATCH 12/22] WIP --- prompts/critical_systems_thinking.jinja | 22 ++ .../prompts/critical_systems_thinking.jinja | 44 +++ .../toolkits/critical_systems_thinking.py | 324 +++++++++++++++++- .../prompts/critical_systems_thinking.jinja | 122 ++++++- tests/toolkits/.DS_Store | Bin 0 -> 6148 bytes 5 files changed, 510 insertions(+), 2 deletions(-) create mode 100644 prompts/critical_systems_thinking.jinja create mode 100644 src/goose_plugins/prompts/critical_systems_thinking.jinja create mode 100644 tests/toolkits/.DS_Store diff --git a/prompts/critical_systems_thinking.jinja b/prompts/critical_systems_thinking.jinja new file mode 100644 index 0000000..de90fcb --- /dev/null +++ b/prompts/critical_systems_thinking.jinja @@ -0,0 +1,22 @@ +You are an AI assistant specialized in critical systems thinking. Your primary function is to analyze complex problems, propose solutions, and execute tasks autonomously when required. + +You have the following capabilities: +1. Perform structured analysis using the MECE principle. +2. Search for information using web APIs. +3. Analyze and clarify unclear requests. +4. Review web page content. +5. Consider and propose solutions to problems. +6. Conduct stakeholder analysis. +7. Generate future scenarios. +8. Create system maps. +9. Perform risk assessments. +10. Conduct ethical analyses. +11. Run tasks autonomously in the background. + +When using the autonomous_loop tool: +- You can start a background task that simulates work. +- While the background task is running, you can still respond to new messages from the user. +- When the background task completes, you should provide an update on its completion. +- You should be able to handle multiple tasks in the background and keep track of their progress. + +Always strive to provide clear, concise, and well-structured responses. When analyzing problems or proposing solutions, consider multiple perspectives and potential consequences. \ No newline at end of file diff --git a/src/goose_plugins/prompts/critical_systems_thinking.jinja b/src/goose_plugins/prompts/critical_systems_thinking.jinja new file mode 100644 index 0000000..7208f0b --- /dev/null +++ b/src/goose_plugins/prompts/critical_systems_thinking.jinja @@ -0,0 +1,44 @@ +You are an AI assistant specialized in critical systems thinking. Your role is to analyze complex problems and provide comprehensive, anticipatory solutions using a systematic approach. Follow these steps when addressing a problem: + +1. Problem Analysis: + - Use the `analyze_request` tool to clarify the problem statement if needed. + - Employ the `search` tool to gather relevant information about the problem context. + +2. Stakeholder Identification: + - Use the `stakeholder_analysis` tool to identify key stakeholders, their interests, and potential impacts. + +3. System Mapping: + - Apply the `system_mapping` tool to create a high-level map of the system, identifying key components and their relationships. + +4. Structured Analysis: + - Utilize the `structured_analysis` tool to perform a MECE (Mutually Exclusive, Collectively Exhaustive) analysis of the problem. + +5. Future Scenario Generation: + - Use the `generate_future_scenarios` tool to explore potential future outcomes and challenges. + +6. Solution Consideration: + - Apply the `consider_solutions` tool to generate potential solutions based on the comprehensive analysis. + +7. Risk Assessment: + - Use the `risk_assessment` tool to evaluate potential risks associated with the proposed solutions. + +8. Ethical Analysis: + - Employ the `ethical_analysis` tool to assess the ethical implications of the problem and proposed solutions. + +9. Web Research: + - If additional information is needed, use the `review_web_page` tool to analyze relevant web content. + +10. Synthesis and Recommendation: + - Synthesize the insights gained from all previous steps. + - Provide a well-reasoned recommendation that addresses the problem comprehensively, considers future scenarios, mitigates potential risks, and aligns with ethical considerations. + +Remember to: +- Consider long-term consequences and systemic impacts of proposed solutions. +- Identify and analyze feedback loops within the system. +- Address the interconnectedness of various components and stakeholders. +- Anticipate potential unintended consequences of interventions. +- Propose adaptive and flexible solutions that can respond to changing conditions. +- Balance ethical considerations with practical implementation. +- Consider the broader societal and environmental impacts of proposed solutions. + +Your goal is to provide a holistic, forward-thinking analysis and solution that addresses the complex nature of the problem at hand, while maintaining ethical integrity and considering the diverse needs of all stakeholders involved. \ No newline at end of file diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index cde7a56..7fac983 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -5,17 +5,120 @@ from goose.utils.ask import ask_an_ai from goose_plugins.utils.selenium_web_browser import get_web_page_content from goose_plugins.utils.serper_search import serper_search +import queue +import time class CriticalSystemsThinking(Toolkit): """Critical systems thinking toolkit for understanding and solving complex problems.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.task_queue = queue.Queue() + self.autonomous_mode = False + def message_content(self, content: Content) -> Text: if isinstance(content, Text): return content else: return Text(str(content)) + def notify(self, message: str): + """Standardized notification method for concise status updates.""" + self.notifier.status(f"[CST] {message[:50]}...") + + def add_task(self, task): + """Add a task to the queue.""" + self.task_queue.put(task) + + def notify_user(self, message): + """Notify the user when help is needed.""" + # Implement the notification method here (e.g., send an email, push notification, etc.) + print(f"[User Notification] {message}") + + @tool + def autonomous_loop(self, task_description: str, duration: int = 10): + """ + Run a task autonomously in the background and allow for interruptions. + + Args: + task_description (str): A description of the task to be simulated. + duration (int): The duration of the task in seconds (default: 10). + + Returns: + str: A message indicating the task has been started. + """ + def background_task(): + start_time = time.time() + while time.time() - start_time < duration: + time.sleep(1) + if not self.autonomous_mode: + return f"Task '{task_description}' was interrupted and stopped." + return f"Task '{task_description}' completed successfully after {duration} seconds." + + self.autonomous_mode = True + self.notify(f"Starting background task: {task_description}") + + # Start the background task in a separate thread + import threading + thread = threading.Thread(target=lambda: self.add_task(background_task())) + thread.start() + + return f"Background task '{task_description}' has been started and will run for approximately {duration} seconds. You can continue interacting while it's running." + + def add_task(self, task): + """Add a task result to the queue.""" + self.task_queue.put(task) + + def check_task_status(self): + """Check if any tasks have completed and return their results.""" + completed_tasks = [] + while not self.task_queue.empty(): + completed_tasks.append(self.task_queue.get()) + return completed_tasks + + @tool + def structured_analysis(self, problem: str) -> str: + """ + Perform a structured analysis of the given problem using the MECE principle. + + Args: + problem (str): A description of the problem to analyze. + + Returns: + response (str): A JSON string containing the structured analysis. + """ + self.notify("Performing structured analysis") + + provider = AnthropicProvider.from_env() + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=[], + system=None + ) + + request_input = f""" + Perform a structured analysis of the following problem using the MECE (Mutually Exclusive, Collectively Exhaustive) principle: + {problem} + + Return the results as a JSON string with the following structure: + {{ + "problem": "Brief restatement of the problem", + "categories": [ + {{ + "name": "Category Name", + "elements": ["Element 1", "Element 2", ...], + "analysis": "Brief analysis of this category" + }}, + ... + ], + "conclusion": "Overall conclusion based on the structured analysis" + }} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + @tool def search(self, query: str) -> str: """ @@ -139,6 +242,225 @@ def consider_solutions(self, statement: str) -> str: response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) return response.content[0].text + @tool + def stakeholder_analysis(self, problem_statement: str) -> str: + """ + Identify and analyze key stakeholders related to the given problem. + + Args: + problem_statement (str): A description of the problem or situation. + + Returns: + response (str): A JSON string containing a list of stakeholders, their interests, and potential impacts. + """ + self.notifier.status("Analyzing stakeholders...") + + provider = AnthropicProvider.from_env() + + 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="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None + ) + + request_input = f""" + Analyze the following problem statement and identify key stakeholders: + {problem_statement} + For each stakeholder, determine their interests and potential impacts. + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + + @tool + def generate_future_scenarios(self, problem_statement: str, time_horizon: str) -> str: + """ + Generate potential future scenarios based on the given problem statement and time horizon. + + Args: + problem_statement (str): A description of the current problem or situation. + time_horizon (str): The future time frame to consider (e.g., "5 years", "10 years", "50 years"). + + Returns: + response (str): A JSON string containing a list of potential future scenarios. + """ + self.notifier.status("Generating future scenarios...") + + provider = AnthropicProvider.from_env() + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=[], + system=None + ) + + request_input = f""" + Based on the following problem statement and time horizon, generate potential future scenarios: + Problem: {problem_statement} + Time Horizon: {time_horizon} + + Consider various factors such as technological advancements, societal changes, environmental impacts, and potential policy shifts. + + Return the results as a JSON string with the following structure: + {{ + "scenarios": [ + {{ + "name": "Scenario Name", + "description": "Brief description of the scenario", + "key_factors": ["Factor 1", "Factor 2", ...], + "potential_outcomes": ["Outcome 1", "Outcome 2", ...] + }}, + ... + ] + }} + + Generate at least 3 distinct scenarios. + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + + @tool + def system_mapping(self, problem_statement: str) -> str: + """ + Create a high-level system map based on the given problem statement. + + Args: + problem_statement (str): A description of the problem or situation. + + Returns: + response (str): A JSON string representing a high-level system map. + """ + self.notifier.status("Creating system map...") + + provider = AnthropicProvider.from_env() + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=[], + system=None + ) + + request_input = f""" + Based on the following problem statement, create a high-level system map: + {problem_statement} + + Identify key components, their relationships, and potential feedback loops. + Return the results as a JSON string with the following structure: + {{ + "components": [ + {{ + "name": "Component Name", + "description": "Brief description of the component", + "connections": ["Component 1", "Component 2", ...] + }}, + ... + ], + "feedback_loops": [ + {{ + "name": "Loop Name", + "description": "Description of the feedback loop", + "components_involved": ["Component 1", "Component 2", ...] + }}, + ... + ] + }} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + + @tool + def risk_assessment(self, problem_statement: str, proposed_solution: str) -> str: + """ + Perform a risk assessment for the given problem and proposed solution. + + Args: + problem_statement (str): A description of the problem or situation. + proposed_solution (str): A description of the proposed solution. + + Returns: + response (str): A JSON string containing a list of potential risks and their assessments. + """ + self.notifier.status("Performing risk assessment...") + + provider = AnthropicProvider.from_env() + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=[], + system=None + ) + + request_input = f""" + Perform a risk assessment for the following problem and proposed solution: + Problem: {problem_statement} + Proposed Solution: {proposed_solution} + + Identify potential risks, their likelihood, impact, and possible mitigation strategies. + Return the results as a JSON string with the following structure: + {{ + "risks": [ + {{ + "name": "Risk Name", + "description": "Description of the risk", + "likelihood": "High/Medium/Low", + "impact": "High/Medium/Low", + "mitigation_strategies": ["Strategy 1", "Strategy 2", ...] + }}, + ... + ] + }} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + + @tool + def ethical_analysis(self, problem_statement: str, proposed_solution: str) -> str: + """ + Perform an ethical analysis of the given problem and proposed solution. + + Args: + problem_statement (str): A description of the problem or situation. + proposed_solution (str): A description of the proposed solution. + + Returns: + response (str): A JSON string containing an ethical analysis of the problem and solution. + """ + self.notifier.status("Performing ethical analysis...") + + provider = AnthropicProvider.from_env() + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=[], + system=None + ) + + request_input = f""" + Perform an ethical analysis for the following problem and proposed solution: + Problem: {problem_statement} + Proposed Solution: {proposed_solution} + + Consider various ethical frameworks and principles, potential ethical dilemmas, and the impact on different stakeholders. + Return the results as a JSON string with the following structure: + {{ + "ethical_considerations": [ + {{ + "principle": "Ethical Principle", + "description": "Description of the ethical consideration", + "impact": "Positive/Negative/Neutral", + "affected_stakeholders": ["Stakeholder 1", "Stakeholder 2", ...], + "recommendations": ["Recommendation 1", "Recommendation 2", ...] + }}, + ... + ], + "overall_assessment": "Summary of the ethical analysis and recommendations" + }} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + def system_prompt(self) -> str: """Retrieve instructions on how to use this reasoning tool.""" - return Message.load("prompts/critical_systems_thinking.jinja").text + return Message.load("prompts/critical_systems_thinking.jinja").text \ No newline at end of file diff --git a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja index 5b963fc..09ef039 100644 --- a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja +++ b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja @@ -37,4 +37,124 @@ - Propose adaptive and flexible solutions that can respond to changing conditions. Your goal is to provide a holistic, forward-thinking analysis and solution that addresses the complex nature of the - problem at hand. \ No newline at end of file + problem at hand. + + # Operating Principles + +# Response Tiers + +Respond in three tiers based on query complexity: +1. Simple (1-2 sentences) → Direct, precise response +2. Technical/practical → Structured analysis with explicit reasoning +3. Complex systems → Full critical systems analysis +4. Tool required for response → Use appropriate tool in toolkit + +# Technical & Practical Reasoning Framework +When addressing technical decisions or practical problems: + +1. Context Assessment +- Identify technical constraints and requirements +- Surface implicit assumptions +- Map dependencies and interfaces +- Note reliability/safety considerations + +2. Solution Space Analysis +- Generate multiple viable approaches +- Compare tradeoffs explicitly: + - Performance characteristics + - Implementation complexity + - Maintenance requirements + - Risk factors +- Test assumptions with "what if" scenarios + +3. Real-World Considerations +- Resource constraints (time, cost, skills) +- Operational impacts +- Future flexibility needs +- Common failure modes + +# Critical Systems Analysis Process +For complex system-level problems: + +1. Initial Rapid Assessment +- Core components + key interactions +- Immediate constraints + dynamics +- Critical assumptions to challenge + +2. Deep Systems Analysis +- Hidden variables + feedback loops +- Second/third-order effects +- System leverage points +- Emergent behaviors +- Stability characteristics + +3. Solution Development +- Multiple intervention points +- Phased implementation paths +- Success metrics +- Risk mitigation strategies + +# Response Structure +Frame analysis as: +Problem → Constraints → Options → Tradeoffs → Recommended Path + +Support with: +- Explicit "because" reasoning +- Concrete examples +- Edge case consideration +- Implementation guidance + +# Communication Principles +- Mirror human's technical depth while maintaining clarity +- Acknowledge uncertainty explicitly +- Seek clarification on ambiguous requirements +- Surface hidden complexities respectfully +- Provide actionable next steps + +# Expert Domain Behavior +When operating in technical domains: +- Apply domain-specific best practices +- Reference relevant patterns/anti-patterns +- Consider maintenance/operations impacts +- Flag security/reliability concerns +- Note compliance requirements +- Suggest monitoring/validation approaches + +# Common Sense Guidelines +When addressing practical problems: +- Consider human factors and usability +- Account for resource constraints +- Anticipate likely failure modes +- Suggest simple solutions first +- Provide fallback options +- Consider deployment context + +# Tools + +If you are asked a basic question that can be without at tool, respond accurately and concisely. + +If you need to use a tool to respond to a request, return the tool response with no additional commentary. + +If you need to use a tool to respond to a request, use the appropriate tool your toolkit. + +If you need to use a tool to respond to a request, in your response before invoking the tool only say "Using tool: [tool_name]". + +If you are asked a question you can answer without a tool and in a short response, do not use a tool. + +If you do not have a tool to respond to a query, describe the type of tool you would need to be able to respond to the query. + +If you are asked about something that requires realtime information about the world use the "serper_search" tool defined in your toolkit. + +If you are asked a request that is unclear, very broad, or not specific enough, use the "analyze_request" tool defined in your toolkit. + +If you think there may be a misunderstanding or typo in the request, use the "analyze_request" tool to clarify the request. + +If you are provided with answers to your questions, but does not sufficiently answer all of your questions, use the "analyze_request" tool defined in your toolkit. + +If the user answers all of your questions sufficiently to move forward with a response, use the "consider_solutions" tool defined in your toolkit. + +If the user asks you to review a web page and gives you a url use the "review_web_page" tool defined in your toolkit. + +If you invoke the review web page you should pass a single url to the tool. If you are given multiple urls, you should ask the user to provide a single url. + +If you are asked about something and given a url always use the review web page tool to respond. \ No newline at end of file diff --git a/tests/toolkits/.DS_Store b/tests/toolkits/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Sat, 2 Nov 2024 01:48:41 -0400 Subject: [PATCH 13/22] WIP --- .../toolkits/critical_systems_thinking.py | 70 +++++++++++++++---- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index 7fac983..7deab2a 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -7,6 +7,7 @@ from goose_plugins.utils.serper_search import serper_search import queue import time +import threading class CriticalSystemsThinking(Toolkit): @@ -16,6 +17,8 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.task_queue = queue.Queue() self.autonomous_mode = False + self.ongoing_tasks = {} + self.completed_tasks = [] def message_content(self, content: Content) -> Text: if isinstance(content, Text): @@ -27,9 +30,21 @@ def notify(self, message: str): """Standardized notification method for concise status updates.""" self.notifier.status(f"[CST] {message[:50]}...") - def add_task(self, task): - """Add a task to the queue.""" - self.task_queue.put(task) + def add_task(self, task_id, task_description, duration): + """Add a task to the ongoing tasks dictionary.""" + self.ongoing_tasks[task_id] = { + "description": task_description, + "start_time": time.time(), + "duration": duration + } + + def complete_task(self, task_id, result): + """Move a task from ongoing to completed.""" + if task_id in self.ongoing_tasks: + task = self.ongoing_tasks.pop(task_id) + task["result"] = result + task["end_time"] = time.time() + self.completed_tasks.append(task) def notify_user(self, message): """Notify the user when help is needed.""" @@ -48,34 +63,59 @@ def autonomous_loop(self, task_description: str, duration: int = 10): Returns: str: A message indicating the task has been started. """ + task_id = f"task_{int(time.time())}" + def background_task(): start_time = time.time() while time.time() - start_time < duration: time.sleep(1) if not self.autonomous_mode: - return f"Task '{task_description}' was interrupted and stopped." - return f"Task '{task_description}' completed successfully after {duration} seconds." + self.complete_task(task_id, f"Task '{task_description}' was interrupted and stopped.") + return + self.complete_task(task_id, f"Task '{task_description}' completed successfully after {duration} seconds.") self.autonomous_mode = True self.notify(f"Starting background task: {task_description}") + self.add_task(task_id, task_description, duration) # Start the background task in a separate thread - import threading - thread = threading.Thread(target=lambda: self.add_task(background_task())) + thread = threading.Thread(target=background_task) thread.start() - return f"Background task '{task_description}' has been started and will run for approximately {duration} seconds. You can continue interacting while it's running." + return f"Background task '{task_description}' (ID: {task_id}) has been started and will run for approximately {duration} seconds. You can continue interacting while it's running." + + @tool + def get_background_job_status(self): + """ + Get the status of all background jobs (ongoing and completed). - def add_task(self, task): - """Add a task result to the queue.""" - self.task_queue.put(task) + Returns: + str: A formatted string containing the status of all background jobs. + """ + current_time = time.time() + status = "Background Job Status:\n\n" + + status += "Ongoing Tasks:\n" + for task_id, task in self.ongoing_tasks.items(): + elapsed_time = current_time - task["start_time"] + remaining_time = max(0, task["duration"] - elapsed_time) + status += f"- Task ID: {task_id}\n" + status += f" Description: {task['description']}\n" + status += f" Elapsed Time: {elapsed_time:.2f} seconds\n" + status += f" Remaining Time: {remaining_time:.2f} seconds\n\n" + + status += "Completed Tasks:\n" + for task in self.completed_tasks: + status += f"- Task ID: {task['description']}\n" + status += f" Description: {task['description']}\n" + status += f" Duration: {task['end_time'] - task['start_time']:.2f} seconds\n" + status += f" Result: {task['result']}\n\n" + + return status def check_task_status(self): """Check if any tasks have completed and return their results.""" - completed_tasks = [] - while not self.task_queue.empty(): - completed_tasks.append(self.task_queue.get()) - return completed_tasks + return [task["result"] for task in self.completed_tasks] @tool def structured_analysis(self, problem: str) -> str: From 759ec592554736bafcfe2f4c48dcbe20f3191223 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 13:43:07 -0400 Subject: [PATCH 14/22] WIP --- .../toolkits/critical_systems_thinking.py | 485 ++++++---------- src/goose_plugins/toolkits/cst.py | 535 ++++++++++++++++++ .../prompts/critical_systems_thinking.jinja | 6 +- 3 files changed, 726 insertions(+), 300 deletions(-) create mode 100644 src/goose_plugins/toolkits/cst.py diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index 7deab2a..5691840 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -8,7 +8,7 @@ import queue import time import threading - +import os class CriticalSystemsThinking(Toolkit): """Critical systems thinking toolkit for understanding and solving complex problems.""" @@ -20,17 +20,36 @@ def __init__(self, *args, **kwargs): self.ongoing_tasks = {} self.completed_tasks = [] + def _create_exchange(self, include_history=False, system=None) -> Exchange: + """Create a new Exchange instance with optional message history.""" + provider = AnthropicProvider.from_env() + messages = [] + if include_history: + messages = [ + Message(role=msg.role, content=[self.message_content(content) for content in msg.content]) + for msg in self.exchange_view.processor.messages + ] + return Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=messages, + system=system + ) + + def _ask_ai(self, prompt: str, include_history=False, system=None) -> str: + """Helper method to handle AI requests.""" + exchange = self._create_exchange(include_history, system) + response = ask_an_ai(input=prompt, exchange=exchange, no_history=not include_history) + return response.content[0].text + def message_content(self, content: Content) -> Text: - if isinstance(content, Text): - return content - else: - return Text(str(content)) + return Text(str(content)) if not isinstance(content, Text) else content def notify(self, message: str): """Standardized notification method for concise status updates.""" self.notifier.status(f"[CST] {message[:50]}...") - def add_task(self, task_id, task_description, duration): + def add_task(self, task_id: str, task_description: str, duration: int): """Add a task to the ongoing tasks dictionary.""" self.ongoing_tasks[task_id] = { "description": task_description, @@ -38,84 +57,170 @@ def add_task(self, task_id, task_description, duration): "duration": duration } - def complete_task(self, task_id, result): - """Move a task from ongoing to completed.""" + def complete_task(self, task_id: str, result: str): + """Move a task from ongoing to completed and record result.""" if task_id in self.ongoing_tasks: task = self.ongoing_tasks.pop(task_id) - task["result"] = result - task["end_time"] = time.time() + task.update({"result": result, "end_time": time.time()}) self.completed_tasks.append(task) - def notify_user(self, message): + def notify_user(self, message: str): """Notify the user when help is needed.""" - # Implement the notification method here (e.g., send an email, push notification, etc.) print(f"[User Notification] {message}") + def _parse_duration(self, duration: str) -> int: + """Convert duration string to seconds.""" + value = int(duration[:-1]) + unit = duration[-1].lower() + multipliers = {'s': 1, 'm': 60, 'h': 3600} + if unit not in multipliers: + raise ValueError("Invalid duration format. Use 's' for seconds, 'm' for minutes, or 'h' for hours.") + return value * multipliers[unit] + + def _trigger_apple_dialog(self, title: str, message: str): + """Trigger an Apple dialog box.""" + dialog_command = f'display dialog "{message}" buttons {{"OK"}} default button "OK" with title "{title}"' + os.system(f"osascript -e '{dialog_command}'") + @tool - def autonomous_loop(self, task_description: str, duration: int = 10): + def autonomous_loop(self, task_description: str, duration: str = "10s") -> str: """ - Run a task autonomously in the background and allow for interruptions. + Run a task autonomously in the background and trigger an Apple dialog box after the specified duration. Args: task_description (str): A description of the task to be simulated. - duration (int): The duration of the task in seconds (default: 10). + duration (str): The duration of the task. Format: "". + Units: 's' for seconds, 'm' for minutes, 'h' for hours. + Examples: "30s", "5m", "1h". Default: "10s". Returns: str: A message indicating the task has been started. """ + duration_seconds = self._parse_duration(duration) task_id = f"task_{int(time.time())}" def background_task(): - start_time = time.time() - while time.time() - start_time < duration: - time.sleep(1) - if not self.autonomous_mode: - self.complete_task(task_id, f"Task '{task_description}' was interrupted and stopped.") - return - self.complete_task(task_id, f"Task '{task_description}' completed successfully after {duration} seconds.") + time.sleep(duration_seconds) + response = self._ask_ai(f"Simulate a response to task_description: {task_description}") + self._trigger_apple_dialog("Task Completed", "Task completed") + self.complete_task(task_id, f"Task '{task_description}' completed.") self.autonomous_mode = True self.notify(f"Starting background task: {task_description}") - self.add_task(task_id, task_description, duration) + self.add_task(task_id, task_description, duration_seconds) - # Start the background task in a separate thread - thread = threading.Thread(target=background_task) - thread.start() - - return f"Background task '{task_description}' (ID: {task_id}) has been started and will run for approximately {duration} seconds. You can continue interacting while it's running." - - @tool - def get_background_job_status(self): - """ - Get the status of all background jobs (ongoing and completed). - - Returns: - str: A formatted string containing the status of all background jobs. - """ - current_time = time.time() - status = "Background Job Status:\n\n" - - status += "Ongoing Tasks:\n" - for task_id, task in self.ongoing_tasks.items(): - elapsed_time = current_time - task["start_time"] - remaining_time = max(0, task["duration"] - elapsed_time) - status += f"- Task ID: {task_id}\n" - status += f" Description: {task['description']}\n" - status += f" Elapsed Time: {elapsed_time:.2f} seconds\n" - status += f" Remaining Time: {remaining_time:.2f} seconds\n\n" - - status += "Completed Tasks:\n" - for task in self.completed_tasks: - status += f"- Task ID: {task['description']}\n" - status += f" Description: {task['description']}\n" + threading.Thread(target=background_task).start() + return f"Background task '{task_description}' (ID: {task_id}) has been started with a duration of {duration}." + + def _format_task_status(self, task_id: str, task: dict, current_time: float) -> str: + """Format a single task's status.""" + elapsed_time = current_time - task["start_time"] + remaining_time = max(0, task.get("duration", 0) - elapsed_time) + status = f"- Task ID: {task_id}\n" + status += f" Description: {task['description']}\n" + if "end_time" in task: status += f" Duration: {task['end_time'] - task['start_time']:.2f} seconds\n" - status += f" Result: {task['result']}\n\n" - + status += f" Result: {task['result']}\n" + else: + status += f" Elapsed Time: {elapsed_time:.2f} seconds\n" + status += f" Remaining Time: {remaining_time:.2f} seconds\n" return status - def check_task_status(self): - """Check if any tasks have completed and return their results.""" - return [task["result"] for task in self.completed_tasks] + @tool + def get_background_job_status(self) -> str: + """Get the status of all background jobs (ongoing and completed).""" + current_time = time.time() + status = ["Background Job Status:\n"] + + if self.ongoing_tasks: + status.append("\nOngoing Tasks:") + for task_id, task in self.ongoing_tasks.items(): + status.append(self._format_task_status(task_id, task, current_time)) + + if self.completed_tasks: + status.append("\nCompleted Tasks:") + for task in self.completed_tasks: + status.append(self._format_task_status(task["description"], task, current_time)) + + return "\n".join(status) + + def _create_analysis_prompt(self, analysis_type: str, **kwargs) -> str: + """Create a prompt for various types of analysis.""" + prompts = { + "structured": lambda p: f""" + Perform a structured analysis of the following problem using the MECE principle: + {p} + Return the results as a JSON string with the following structure: + {{ + "problem": "Brief restatement of the problem", + "categories": [ + {{ + "name": "Category Name", + "elements": ["Element 1", "Element 2", ...], + "analysis": "Brief analysis of this category" + }}, + ... + ], + "conclusion": "Overall conclusion based on the structured analysis" + }} + """, + "request": lambda s: f""" + Analyze the user statement: {s} + If you need to immediately clarify something and it's something + short and simple, respond with your question(s). + If you need multiple questions, you can ask multiple questions. + Please bullet point your questions. + Limit your response to 5 questions. + """, + "solutions": lambda s: f""" + Analyze the user statement: {s} + Consider the existing message history and provide a well thought out response. + Provide one or more potential solutions to the problem. + Limit your response to 5 solutions. + """, + "stakeholder": lambda p: f""" + Analyze the following problem statement and identify key stakeholders: + {p} + For each stakeholder, determine their interests and potential impacts. + """, + "future_scenarios": lambda p, t: f""" + Based on the following problem statement and time horizon, generate potential future scenarios: + Problem: {p} + Time Horizon: {t} + + Consider various factors such as technological advancements, societal changes, + environmental impacts, and potential policy shifts. + + Return the results as a JSON string with scenarios including name, description, + key factors, and potential outcomes. + Generate at least 3 distinct scenarios. + """, + "system_mapping": lambda p: f""" + Based on the following problem statement, create a high-level system map: + {p} + + Identify key components, their relationships, and potential feedback loops. + Return results as JSON with components and feedback loops. + """, + "risk": lambda p, s: f""" + Perform a risk assessment for the following problem and proposed solution: + Problem: {p} + Proposed Solution: {s} + + Identify potential risks, their likelihood, impact, and possible mitigation strategies. + Return results as JSON with detailed risk assessments. + """, + "ethical": lambda p, s: f""" + Perform an ethical analysis for the following problem and proposed solution: + Problem: {p} + Proposed Solution: {s} + + Consider various ethical frameworks and principles, potential ethical dilemmas, + and the impact on different stakeholders. + Return results as JSON with ethical considerations and overall assessment. + """ + } + return prompts[analysis_type](**kwargs) @tool def structured_analysis(self, problem: str) -> str: @@ -129,40 +234,12 @@ def structured_analysis(self, problem: str) -> str: response (str): A JSON string containing the structured analysis. """ self.notify("Performing structured analysis") - - provider = AnthropicProvider.from_env() - exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=[], - system=None - ) - - request_input = f""" - Perform a structured analysis of the following problem using the MECE (Mutually Exclusive, Collectively Exhaustive) principle: - {problem} - - Return the results as a JSON string with the following structure: - {{ - "problem": "Brief restatement of the problem", - "categories": [ - {{ - "name": "Category Name", - "elements": ["Element 1", "Element 2", ...], - "analysis": "Brief analysis of this category" - }}, - ... - ], - "conclusion": "Overall conclusion based on the structured analysis" - }} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text + return self._ask_ai(self._create_analysis_prompt("structured", p=problem)) @tool def search(self, query: str) -> str: """ - Search the web for information using the Serper API. This will return a list of search results. + Search the web for information using the Serper API. Args: query (str): query to search for. @@ -171,15 +248,13 @@ def search(self, query: str) -> str: response (str): A JSON response containing search results. """ self.notifier.status("searching...") - return serper_search(query) @tool def analyze_request(self, statement: str) -> str: """ When a request is unclear, high-level or ambiguous use this tool to - analyze the response and provide a well thought out response. You should - return a well thought out response to the statement or question. + analyze the response and provide a well thought out response. Args: statement (str): description of problem or errors seen. @@ -187,34 +262,13 @@ def analyze_request(self, statement: str) -> str: Returns: response (str): A well thought out response to the statement or question. """ - self.notifier.status("analyzing request...") - - provider = AnthropicProvider.from_env() - - 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="claude-3-5-sonnet-20240620", - messages=existing_messages_copy, - system=self.system_prompt(), + return self._ask_ai( + self._create_analysis_prompt("request", s=statement), + include_history=True, + system=self.system_prompt() ) - request_input = f""" - Analyze the user statement: {statement} - If you need to immediately clarify something and it's something - short and simple, respond with your question(s). - If you need multiple questions, you can ask multiple questions. - Please bullet point your questions. - Limit your response to 5 questions. - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) - return response.content[0].text - @tool def review_web_page(self, url: str) -> str: """ @@ -226,27 +280,14 @@ def review_web_page(self, url: str) -> str: Returns: response (str): A summary of the content of the web page. """ - self.notifier.status(f"fetching content from {url}") - - # Get the text content of the web page - web_content = "" try: web_content = get_web_page_content(url) + self.notifier.status(f"reviewing content...") + return self._ask_ai(f"summarize the following content: {web_content}") except Exception as e: return f"Error: {str(e)}" - self.notifier.status(f"reviewing content: {web_content[:50]}...") - - provider = AnthropicProvider.from_env() - - exchange = Exchange(provider=provider, model="claude-3-5-sonnet-20240620", messages=[], system=None) - request_input = f""" - summarize the following content: {web_content} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) - return response.content[0].text - @tool def consider_solutions(self, statement: str) -> str: """ @@ -259,29 +300,12 @@ def consider_solutions(self, statement: str) -> str: Returns: response (str): A well thought out response to the statement or question. """ - self.notifier.status("considering solutions...") - - provider = AnthropicProvider.from_env() - - 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="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None + return self._ask_ai( + self._create_analysis_prompt("solutions", s=statement), + include_history=True ) - request_input = f""" - Analyze the user statement: {statement} - Consider the existing message history and provide a well thought out response. - Provide one or more potential solutions to the problem. - Limit your response to 5 solutions. - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) - return response.content[0].text - @tool def stakeholder_analysis(self, problem_statement: str) -> str: """ @@ -294,26 +318,11 @@ def stakeholder_analysis(self, problem_statement: str) -> str: response (str): A JSON string containing a list of stakeholders, their interests, and potential impacts. """ self.notifier.status("Analyzing stakeholders...") - - provider = AnthropicProvider.from_env() - - 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="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None + return self._ask_ai( + self._create_analysis_prompt("stakeholder", p=problem_statement), + include_history=True ) - request_input = f""" - Analyze the following problem statement and identify key stakeholders: - {problem_statement} - For each stakeholder, determine their interests and potential impacts. - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text - @tool def generate_future_scenarios(self, problem_statement: str, time_horizon: str) -> str: """ @@ -327,40 +336,10 @@ def generate_future_scenarios(self, problem_statement: str, time_horizon: str) - response (str): A JSON string containing a list of potential future scenarios. """ self.notifier.status("Generating future scenarios...") - - provider = AnthropicProvider.from_env() - exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=[], - system=None + return self._ask_ai( + self._create_analysis_prompt("future_scenarios", p=problem_statement, t=time_horizon) ) - request_input = f""" - Based on the following problem statement and time horizon, generate potential future scenarios: - Problem: {problem_statement} - Time Horizon: {time_horizon} - - Consider various factors such as technological advancements, societal changes, environmental impacts, and potential policy shifts. - - Return the results as a JSON string with the following structure: - {{ - "scenarios": [ - {{ - "name": "Scenario Name", - "description": "Brief description of the scenario", - "key_factors": ["Factor 1", "Factor 2", ...], - "potential_outcomes": ["Outcome 1", "Outcome 2", ...] - }}, - ... - ] - }} - - Generate at least 3 distinct scenarios. - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text - @tool def system_mapping(self, problem_statement: str) -> str: """ @@ -373,42 +352,7 @@ def system_mapping(self, problem_statement: str) -> str: response (str): A JSON string representing a high-level system map. """ self.notifier.status("Creating system map...") - - provider = AnthropicProvider.from_env() - exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=[], - system=None - ) - - request_input = f""" - Based on the following problem statement, create a high-level system map: - {problem_statement} - - Identify key components, their relationships, and potential feedback loops. - Return the results as a JSON string with the following structure: - {{ - "components": [ - {{ - "name": "Component Name", - "description": "Brief description of the component", - "connections": ["Component 1", "Component 2", ...] - }}, - ... - ], - "feedback_loops": [ - {{ - "name": "Loop Name", - "description": "Description of the feedback loop", - "components_involved": ["Component 1", "Component 2", ...] - }}, - ... - ] - }} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text + return self._ask_ai(self._create_analysis_prompt("system_mapping", p=problem_statement)) @tool def risk_assessment(self, problem_statement: str, proposed_solution: str) -> str: @@ -423,38 +367,10 @@ def risk_assessment(self, problem_statement: str, proposed_solution: str) -> str response (str): A JSON string containing a list of potential risks and their assessments. """ self.notifier.status("Performing risk assessment...") - - provider = AnthropicProvider.from_env() - exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=[], - system=None + return self._ask_ai( + self._create_analysis_prompt("risk", p=problem_statement, s=proposed_solution) ) - request_input = f""" - Perform a risk assessment for the following problem and proposed solution: - Problem: {problem_statement} - Proposed Solution: {proposed_solution} - - Identify potential risks, their likelihood, impact, and possible mitigation strategies. - Return the results as a JSON string with the following structure: - {{ - "risks": [ - {{ - "name": "Risk Name", - "description": "Description of the risk", - "likelihood": "High/Medium/Low", - "impact": "High/Medium/Low", - "mitigation_strategies": ["Strategy 1", "Strategy 2", ...] - }}, - ... - ] - }} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text - @tool def ethical_analysis(self, problem_statement: str, proposed_solution: str) -> str: """ @@ -468,39 +384,10 @@ def ethical_analysis(self, problem_statement: str, proposed_solution: str) -> st response (str): A JSON string containing an ethical analysis of the problem and solution. """ self.notifier.status("Performing ethical analysis...") - - provider = AnthropicProvider.from_env() - exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=[], - system=None + return self._ask_ai( + self._create_analysis_prompt("ethical", p=problem_statement, s=proposed_solution) ) - request_input = f""" - Perform an ethical analysis for the following problem and proposed solution: - Problem: {problem_statement} - Proposed Solution: {proposed_solution} - - Consider various ethical frameworks and principles, potential ethical dilemmas, and the impact on different stakeholders. - Return the results as a JSON string with the following structure: - {{ - "ethical_considerations": [ - {{ - "principle": "Ethical Principle", - "description": "Description of the ethical consideration", - "impact": "Positive/Negative/Neutral", - "affected_stakeholders": ["Stakeholder 1", "Stakeholder 2", ...], - "recommendations": ["Recommendation 1", "Recommendation 2", ...] - }}, - ... - ], - "overall_assessment": "Summary of the ethical analysis and recommendations" - }} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text - def system_prompt(self) -> str: """Retrieve instructions on how to use this reasoning tool.""" - return Message.load("prompts/critical_systems_thinking.jinja").text \ No newline at end of file + return Message.load("prompts/critical_systems_thinking.jinja").text diff --git a/src/goose_plugins/toolkits/cst.py b/src/goose_plugins/toolkits/cst.py new file mode 100644 index 0000000..eaa04c9 --- /dev/null +++ b/src/goose_plugins/toolkits/cst.py @@ -0,0 +1,535 @@ +from exchange import Exchange, Message, Text +from exchange.content import Content +from exchange.providers import AnthropicProvider +from goose.toolkit.base import Toolkit, tool +from goose.utils.ask import ask_an_ai +from goose_plugins.utils.selenium_web_browser import get_web_page_content +from goose_plugins.utils.serper_search import serper_search +import queue +import time +import threading +import os + + +class CriticalSystemsThinking(Toolkit): + """Critical systems thinking toolkit for understanding and solving complex problems.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.task_queue = queue.Queue() + self.autonomous_mode = False + self.ongoing_tasks = {} + self.completed_tasks = [] + + def message_content(self, content: Content) -> Text: + if isinstance(content, Text): + return content + else: + return Text(str(content)) + + def notify(self, message: str): + """Standardized notification method for concise status updates.""" + self.notifier.status(f"[CST] {message[:50]}...") + + def add_task(self, task_id, task_description, duration): + """Add a task to the ongoing tasks dictionary.""" + self.ongoing_tasks[task_id] = { + "description": task_description, + "start_time": time.time(), + "duration": duration + } + + def complete_task(self, task_id, result): + """Move a task from ongoing to completed and post a message in the chat.""" + if task_id in self.ongoing_tasks: + task = self.ongoing_tasks.pop(task_id) + task["result"] = result + task["end_time"] = time.time() + self.completed_tasks.append(task) + # self.post_to_chat(task_id, f"Task '{task['description']}' has completed. Result: {result}") + + # def post_to_chat(self, task_id, message): + # """Post a message to the chat.""" + # self.notifier.status(f"[Task {task_id}] {message}") + # print(f"[Task {task_id}] {message}") + # self.notifier.status(f"[Task {task_id}] {message}") + + + def notify_user(self, message): + """Notify the user when help is needed.""" + # Implement the notification method here (e.g., send an email, push notification, etc.) + print(f"[User Notification] {message}") + + @tool + def autonomous_loop(self, task_description: str, duration: str = "10s"): + """ + Run a task autonomously in the background and trigger an Apple dialog box after the specified duration. + + Args: + task_description (str): A description of the task to be simulated. + duration (str): The duration of the task. Format: "". + Units: 's' for seconds, 'm' for minutes, 'h' for hours. + Examples: "30s", "5m", "1h". Default: "10s". + + Returns: + str: A message indicating the task has been started. + """ + # Convert duration string to seconds + duration_value = int(duration[:-1]) + duration_unit = duration[-1].lower() + if duration_unit == 's': + duration_seconds = duration_value + elif duration_unit == 'm': + duration_seconds = duration_value * 60 + elif duration_unit == 'h': + duration_seconds = duration_value * 3600 + else: + raise ValueError("Invalid duration format. Use 's' for seconds, 'm' for minutes, or 'h' for hours.") + task_id = f"task_{int(time.time())}" + + def background_task(): + time.sleep(duration_seconds) + + provider = AnthropicProvider.from_env() + + exchange = Exchange(provider=provider, model="claude-3-5-sonnet-20240620", messages=[], system=None) + request_input = f""" + Simulate a response to task_description: {task_description} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) + response_text = response.content[0].text + # Trigger the Apple dialog using osascript + dialog_command = f'display dialog "Task completed" buttons {{"OK"}} default button "OK" with title "Task Completed"' + os.system(f"osascript -e '{dialog_command}'") + self.complete_task(task_id, f"Task '{task_description}' completed.") + + self.autonomous_mode = True + self.notify(f"Starting background task: {task_description}") + self.add_task(task_id, task_description, duration_seconds) + + # Start the background task in a separate thread + thread = threading.Thread(target=background_task) + thread.start() + + return f"Background task '{task_description}' (ID: {task_id}) has been started with a duration of {duration}. You can continue interacting while it's running." + + @tool + def get_background_job_status(self): + """ + Get the status of all background jobs (ongoing and completed). + + Returns: + str: A formatted string containing the status of all background jobs. + """ + current_time = time.time() + status = "Background Job Status:\n\n" + + status += "Ongoing Tasks:\n" + for task_id, task in self.ongoing_tasks.items(): + elapsed_time = current_time - task["start_time"] + remaining_time = max(0, task["duration"] - elapsed_time) + status += f"- Task ID: {task_id}\n" + status += f" Description: {task['description']}\n" + status += f" Elapsed Time: {elapsed_time:.2f} seconds\n" + status += f" Remaining Time: {remaining_time:.2f} seconds\n\n" + + status += "Completed Tasks:\n" + for task in self.completed_tasks: + status += f"- Task ID: {task['description']}\n" + status += f" Description: {task['description']}\n" + status += f" Duration: {task['end_time'] - task['start_time']:.2f} seconds\n" + status += f" Result: {task['result']}\n\n" + + return status + + def check_task_status(self): + """Check if any tasks have completed and return their results.""" + return [task["result"] for task in self.completed_tasks] + + @tool + def structured_analysis(self, problem: str) -> str: + """ + Perform a structured analysis of the given problem using the MECE principle. + + Args: + problem (str): A description of the problem to analyze. + + Returns: + response (str): A JSON string containing the structured analysis. + """ + self.notify("Performing structured analysis") + + provider = AnthropicProvider.from_env() + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=[], + system=None + ) + + request_input = f""" + Perform a structured analysis of the following problem using the MECE (Mutually Exclusive, Collectively Exhaustive) principle: + {problem} + + Return the results as a JSON string with the following structure: + {{ + "problem": "Brief restatement of the problem", + "categories": [ + {{ + "name": "Category Name", + "elements": ["Element 1", "Element 2", ...], + "analysis": "Brief analysis of this category" + }}, + ... + ], + "conclusion": "Overall conclusion based on the structured analysis" + }} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + + @tool + def search(self, query: str) -> str: + """ + Search the web for information using the Serper API. This will return a list of search results. + + Args: + query (str): query to search for. + + Returns: + response (str): A JSON response containing search results. + """ + self.notifier.status("searching...") + + return serper_search(query) + + @tool + def analyze_request(self, statement: str) -> str: + """ + When a request is unclear, high-level or ambiguous use this tool to + analyze the response and provide a well thought out response. You should + return a well thought out response to the statement or question. + + Args: + statement (str): description of problem or errors seen. + + Returns: + response (str): A well thought out response to the statement or question. + """ + + self.notifier.status("analyzing request...") + + provider = AnthropicProvider.from_env() + + 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="claude-3-5-sonnet-20240620", + messages=existing_messages_copy, + system=self.system_prompt(), + ) + + request_input = f""" + Analyze the user statement: {statement} + If you need to immediately clarify something and it's something + short and simple, respond with your question(s). + If you need multiple questions, you can ask multiple questions. + Please bullet point your questions. + Limit your response to 5 questions. + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) + return response.content[0].text + + @tool + def review_web_page(self, url: str) -> str: + """ + Review the content of a web page by providing a summary of the content. + + Args: + url (str): URL of the web page to review. + + Returns: + response (str): A summary of the content of the web page. + """ + + self.notifier.status(f"fetching content from {url}") + + # Get the text content of the web page + web_content = "" + try: + web_content = get_web_page_content(url) + except Exception as e: + return f"Error: {str(e)}" + + self.notifier.status(f"reviewing content: {web_content[:50]}...") + + provider = AnthropicProvider.from_env() + + exchange = Exchange(provider=provider, model="claude-3-5-sonnet-20240620", messages=[], system=None) + request_input = f""" + summarize the following content: {web_content} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) + return response.content[0].text + + @tool + def consider_solutions(self, statement: str) -> str: + """ + Provide a well thought out response to the statement summarize the + problem and provide a solution or a set of solutions. + + Args: + statement (str): description of problem or errors seen. + + Returns: + response (str): A well thought out response to the statement or question. + """ + + self.notifier.status("considering solutions...") + + provider = AnthropicProvider.from_env() + + 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="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None + ) + + request_input = f""" + Analyze the user statement: {statement} + Consider the existing message history and provide a well thought out response. + Provide one or more potential solutions to the problem. + Limit your response to 5 solutions. + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) + return response.content[0].text + + @tool + def stakeholder_analysis(self, problem_statement: str) -> str: + """ + Identify and analyze key stakeholders related to the given problem. + + Args: + problem_statement (str): A description of the problem or situation. + + Returns: + response (str): A JSON string containing a list of stakeholders, their interests, and potential impacts. + """ + self.notifier.status("Analyzing stakeholders...") + + provider = AnthropicProvider.from_env() + + 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="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None + ) + + request_input = f""" + Analyze the following problem statement and identify key stakeholders: + {problem_statement} + For each stakeholder, determine their interests and potential impacts. + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + + @tool + def generate_future_scenarios(self, problem_statement: str, time_horizon: str) -> str: + """ + Generate potential future scenarios based on the given problem statement and time horizon. + + Args: + problem_statement (str): A description of the current problem or situation. + time_horizon (str): The future time frame to consider (e.g., "5 years", "10 years", "50 years"). + + Returns: + response (str): A JSON string containing a list of potential future scenarios. + """ + self.notifier.status("Generating future scenarios...") + + provider = AnthropicProvider.from_env() + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=[], + system=None + ) + + request_input = f""" + Based on the following problem statement and time horizon, generate potential future scenarios: + Problem: {problem_statement} + Time Horizon: {time_horizon} + + Consider various factors such as technological advancements, societal changes, environmental impacts, and potential policy shifts. + + Return the results as a JSON string with the following structure: + {{ + "scenarios": [ + {{ + "name": "Scenario Name", + "description": "Brief description of the scenario", + "key_factors": ["Factor 1", "Factor 2", ...], + "potential_outcomes": ["Outcome 1", "Outcome 2", ...] + }}, + ... + ] + }} + + Generate at least 3 distinct scenarios. + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + + @tool + def system_mapping(self, problem_statement: str) -> str: + """ + Create a high-level system map based on the given problem statement. + + Args: + problem_statement (str): A description of the problem or situation. + + Returns: + response (str): A JSON string representing a high-level system map. + """ + self.notifier.status("Creating system map...") + + provider = AnthropicProvider.from_env() + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=[], + system=None + ) + + request_input = f""" + Based on the following problem statement, create a high-level system map: + {problem_statement} + + Identify key components, their relationships, and potential feedback loops. + Return the results as a JSON string with the following structure: + {{ + "components": [ + {{ + "name": "Component Name", + "description": "Brief description of the component", + "connections": ["Component 1", "Component 2", ...] + }}, + ... + ], + "feedback_loops": [ + {{ + "name": "Loop Name", + "description": "Description of the feedback loop", + "components_involved": ["Component 1", "Component 2", ...] + }}, + ... + ] + }} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + + @tool + def risk_assessment(self, problem_statement: str, proposed_solution: str) -> str: + """ + Perform a risk assessment for the given problem and proposed solution. + + Args: + problem_statement (str): A description of the problem or situation. + proposed_solution (str): A description of the proposed solution. + + Returns: + response (str): A JSON string containing a list of potential risks and their assessments. + """ + self.notifier.status("Performing risk assessment...") + + provider = AnthropicProvider.from_env() + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=[], + system=None + ) + + request_input = f""" + Perform a risk assessment for the following problem and proposed solution: + Problem: {problem_statement} + Proposed Solution: {proposed_solution} + + Identify potential risks, their likelihood, impact, and possible mitigation strategies. + Return the results as a JSON string with the following structure: + {{ + "risks": [ + {{ + "name": "Risk Name", + "description": "Description of the risk", + "likelihood": "High/Medium/Low", + "impact": "High/Medium/Low", + "mitigation_strategies": ["Strategy 1", "Strategy 2", ...] + }}, + ... + ] + }} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + + @tool + def ethical_analysis(self, problem_statement: str, proposed_solution: str) -> str: + """ + Perform an ethical analysis of the given problem and proposed solution. + + Args: + problem_statement (str): A description of the problem or situation. + proposed_solution (str): A description of the proposed solution. + + Returns: + response (str): A JSON string containing an ethical analysis of the problem and solution. + """ + self.notifier.status("Performing ethical analysis...") + + provider = AnthropicProvider.from_env() + exchange = Exchange( + provider=provider, + model="claude-3-5-sonnet-20240620", + messages=[], + system=None + ) + + request_input = f""" + Perform an ethical analysis for the following problem and proposed solution: + Problem: {problem_statement} + Proposed Solution: {proposed_solution} + + Consider various ethical frameworks and principles, potential ethical dilemmas, and the impact on different stakeholders. + Return the results as a JSON string with the following structure: + {{ + "ethical_considerations": [ + {{ + "principle": "Ethical Principle", + "description": "Description of the ethical consideration", + "impact": "Positive/Negative/Neutral", + "affected_stakeholders": ["Stakeholder 1", "Stakeholder 2", ...], + "recommendations": ["Recommendation 1", "Recommendation 2", ...] + }}, + ... + ], + "overall_assessment": "Summary of the ethical analysis and recommendations" + }} + """ + response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) + return response.content[0].text + + def system_prompt(self) -> str: + """Retrieve instructions on how to use this reasoning tool.""" + return Message.load("prompts/critical_systems_thinking.jinja").text \ No newline at end of file diff --git a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja index 09ef039..f740b92 100644 --- a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja +++ b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja @@ -157,4 +157,8 @@ If the user asks you to review a web page and gives you a url use the "review_we If you invoke the review web page you should pass a single url to the tool. If you are given multiple urls, you should ask the user to provide a single url. -If you are asked about something and given a url always use the review web page tool to respond. \ No newline at end of file +If you are asked about something and given a url always use the review web page tool to respond. + +If you are asked about doing a background job only use the background job tool and no others. + +Do not continue any conversation related to the automomous loop until the task has completed. \ No newline at end of file From be9e2ef73f3591928abe05b3d6f3b51f033ceb10 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 13:49:49 -0400 Subject: [PATCH 15/22] Add 'autonomous' mode. --- .../toolkits/critical_systems_thinking.py | 72 ++++++++++++++----- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index 5691840..27051fe 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -83,34 +83,74 @@ def _trigger_apple_dialog(self, title: str, message: str): os.system(f"osascript -e '{dialog_command}'") @tool - def autonomous_loop(self, task_description: str, duration: str = "10s") -> str: + def autonomous_loop(self, task_description: str, interval: str = "30s", max_iterations: int = 10) -> str: """ - Run a task autonomously in the background and trigger an Apple dialog box after the specified duration. + Run a task autonomously in the background, continuously working on a problem and building a final answer. Args: - task_description (str): A description of the task to be simulated. - duration (str): The duration of the task. Format: "". + task_description (str): A description of the task to be performed. + interval (str): The interval between each iteration. Format: "". Units: 's' for seconds, 'm' for minutes, 'h' for hours. - Examples: "30s", "5m", "1h". Default: "10s". + Examples: "30s", "5m", "1h". Default: "30s". + max_iterations (int): The maximum number of iterations to perform. Default: 10. Returns: - str: A message indicating the task has been started. + str: A message indicating the continuous background task has been started. """ - duration_seconds = self._parse_duration(duration) + interval_seconds = self._parse_duration(interval) task_id = f"task_{int(time.time())}" - def background_task(): - time.sleep(duration_seconds) - response = self._ask_ai(f"Simulate a response to task_description: {task_description}") - self._trigger_apple_dialog("Task Completed", "Task completed") - self.complete_task(task_id, f"Task '{task_description}' completed.") + def continuous_background_task(): + iteration = 0 + final_answer = "" + while iteration < max_iterations: + iteration += 1 + self.notify(f"Starting iteration {iteration} of {max_iterations}") + + # Perform analysis and build upon the final answer + analysis = self._perform_analysis(task_description, final_answer) + final_answer += f"\n\nIteration {iteration} result:\n{analysis}" + + self.notify(f"Iteration {iteration} completed. Waiting for {interval} before next iteration.") + time.sleep(interval_seconds) + + self._trigger_apple_dialog("Task Completed", f"Continuous task completed after {max_iterations} iterations.") + self.complete_task(task_id, f"Continuous task '{task_description}' completed. Final answer: {final_answer}") self.autonomous_mode = True - self.notify(f"Starting background task: {task_description}") - self.add_task(task_id, task_description, duration_seconds) + self.notify(f"Starting continuous background task: {task_description}") + self.add_task(task_id, task_description, interval_seconds * max_iterations) + + threading.Thread(target=continuous_background_task).start() + return f"Continuous background task '{task_description}' (ID: {task_id}) has been started with {max_iterations} iterations and {interval} interval." + + def _perform_analysis(self, task_description: str, current_answer: str) -> str: + """ + Perform analysis for a single iteration of the continuous background task. + This method can invoke other tools as needed. + """ + prompt = f""" + Task description: {task_description} + Current progress: {current_answer} + + Analyze the current progress and provide the next step or insight for solving the problem. + You can use any of the available tools to gather more information or perform specific analyses. + Return your analysis and any tool results in a concise manner. + """ + + analysis = self._ask_ai(prompt, include_history=True) + + # Example of invoking other tools within the analysis + if "need more information" in analysis.lower(): + search_query = f"latest information about {task_description}" + search_results = self.search(search_query) + analysis += f"\n\nAdditional information from search:\n{search_results}" + + if "stakeholder analysis needed" in analysis.lower(): + stakeholder_analysis = self.stakeholder_analysis(task_description) + analysis += f"\n\nStakeholder analysis results:\n{stakeholder_analysis}" - threading.Thread(target=background_task).start() - return f"Background task '{task_description}' (ID: {task_id}) has been started with a duration of {duration}." + return analysis def _format_task_status(self, task_id: str, task: dict, current_time: float) -> str: """Format a single task's status.""" From 2cf1ed6f03db5d5796dc7373b7d990da3dfd7923 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 14:57:48 -0400 Subject: [PATCH 16/22] WIP --- src/goose_plugins/cli.py | 15 + .../toolkits/critical_systems_thinking.py | 432 ++++++++++++++++-- src/goose_plugins/utils/log_streamer.py | 26 ++ 3 files changed, 444 insertions(+), 29 deletions(-) create mode 100644 src/goose_plugins/utils/log_streamer.py diff --git a/src/goose_plugins/cli.py b/src/goose_plugins/cli.py index e69de29..793956a 100644 --- a/src/goose_plugins/cli.py +++ b/src/goose_plugins/cli.py @@ -0,0 +1,15 @@ +import click +from goose_plugins.utils.log_streamer import start_log_stream + +@click.group() +def cli(): + pass + +@cli.command() +def stream_log(): + """Stream the current Goose session log in real-time.""" + click.echo("Starting log stream for the current Goose session...") + start_log_stream() + +if __name__ == '__main__': + cli() \ No newline at end of file diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index 27051fe..42ce3d5 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -9,6 +9,10 @@ import time import threading import os +import json +from pathlib import Path +import signal +import atexit class CriticalSystemsThinking(Toolkit): """Critical systems thinking toolkit for understanding and solving complex problems.""" @@ -19,6 +23,51 @@ def __init__(self, *args, **kwargs): self.autonomous_mode = False self.ongoing_tasks = {} self.completed_tasks = [] + self.latest_results = {} + self.pause_flags = {} + self.loop_threads = {} + self.final_results = {} # New attribute to store final results of completed tasks + self.failed_tasks = [] # New attribute to store failed tasks + self.results_folder = os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', 'debug_results') + os.makedirs(self.results_folder, exist_ok=True) + self.log_file = Path.home() / ".goose" / "session_log.json" + self.log_file.parent.mkdir(parents=True, exist_ok=True) + + # Set up signal handler for graceful shutdown + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGTERM, self._signal_handler) + atexit.register(self._cleanup) + + def _signal_handler(self, signum, frame): + """Handle termination signals.""" + self.notify("Received termination signal. Cleaning up...") + self._cleanup() + sys.exit(0) + + def _cleanup(self): + """Clean up all running tasks.""" + self.notify("Cleaning up and aborting all tasks...") + for task_id in list(self.ongoing_tasks.keys()): + self._abort_task(task_id) + self.notify("All tasks aborted.") + + async def log_action(self, action: str, details: str): + """Log an action to the session log file.""" + log_entry = { + "timestamp": time.time(), + "action": action, + "details": details + } + + def _write_intermediate_result(self, task_id: str, iteration: int, result: str): + """Write intermediate results to a file for debugging purposes.""" + filename = f"{task_id}_iteration_{iteration}.txt" + filepath = os.path.join(self.results_folder, filename) + with open(filepath, 'w') as f: + f.write(f"Task ID: {task_id}\n") + f.write(f"Iteration: {iteration}\n") + f.write(f"Result:\n{result}\n") + self.notify(f"Intermediate result written to {filepath}") def _create_exchange(self, include_history=False, system=None) -> Exchange: """Create a new Exchange instance with optional message history.""" @@ -57,12 +106,29 @@ def add_task(self, task_id: str, task_description: str, duration: int): "duration": duration } - def complete_task(self, task_id: str, result: str): - """Move a task from ongoing to completed and record result.""" + def complete_task(self, task_id: str, result: str, success: bool = True): + """Move a task from ongoing to completed or failed and record result.""" if task_id in self.ongoing_tasks: task = self.ongoing_tasks.pop(task_id) task.update({"result": result, "end_time": time.time()}) - self.completed_tasks.append(task) + if success: + self.completed_tasks.append(task) + self.final_results[task_id] = result # Store the final result + else: + self.failed_tasks.append(task) + if task_id in self.latest_results: + del self.latest_results[task_id] # Remove from latest_results as it's now completed or failed + + def _abort_task(self, task_id: str): + """Abort a running task.""" + if task_id in self.ongoing_tasks: + self.pause_flags[task_id] = True + if task_id in self.loop_threads: + self.loop_threads[task_id].join(timeout=1) # Wait for the thread to finish + self.complete_task(task_id, "Task aborted", success=False) + self.notify(f"Task {task_id} aborted.") + else: + self.notify(f"Task {task_id} not found or already completed.") def notify_user(self, message: str): """Notify the user when help is needed.""" @@ -82,47 +148,161 @@ def _trigger_apple_dialog(self, title: str, message: str): dialog_command = f'display dialog "{message}" buttons {{"OK"}} default button "OK" with title "{title}"' os.system(f"osascript -e '{dialog_command}'") + def _call_llm(self, prompt: str) -> str: + """ + Call the language model to refine the answer. + + Args: + prompt (str): The prompt to send to the language model. + + Returns: + response (str): The refined answer from the language model. + """ + return self._ask_ai(prompt, include_history=True, system=self.system_prompt()) + + def _check_stop_condition(self, task_description: str, current_answer: str, iterations: int, elapsed_time: int, total_duration: int) -> dict: + """ + Check if the autonomous loop should stop based on the current state of the task. + + Args: + task_description (str): The description of the task being performed. + current_answer (str): The current refined answer. + iterations (int): The number of iterations completed. + elapsed_time (int): The time elapsed since the task started (in seconds). + total_duration (int): The total allowed duration for the task (in seconds). + + Returns: + dict: A dictionary containing the stop decision and reasoning. + """ + prompt = f""" + Task: {task_description} + Current answer: {current_answer} + Iterations completed: {iterations} + Elapsed time: {elapsed_time} seconds + Total allowed duration: {total_duration} seconds + + Based on the current state of the task, determine if the autonomous loop should stop. + Consider the quality and completeness of the current answer, the number of iterations, + and the time constraints. + + Provide your response in the following JSON format: + {{ + "should_stop": boolean, + "reasoning": "A brief explanation of why the task should or should not stop." + }} + """ + + exchange = self._create_exchange(include_history=True, system=self.system_prompt()) + # exchange.add(Message(role="user", content=[Text(prompt)])) + response = ask_an_ai(input=prompt, exchange=exchange) + + try: + result = json.loads(response.content[0].text) + if not isinstance(result, dict) or "should_stop" not in result or "reasoning" not in result: + raise ValueError("Invalid JSON structure") + return result + except json.JSONDecodeError: + self.notify("Error: Invalid JSON response from LLM for stop condition check.") + return {"should_stop": False, "reasoning": "Error in LLM response format. Continuing task."} + except ValueError as e: + self.notify(f"Error: {str(e)}") + return {"should_stop": False, "reasoning": "Error in LLM response structure. Continuing task."} + @tool - def autonomous_loop(self, task_description: str, interval: str = "30s", max_iterations: int = 10) -> str: + def autonomous_loop(self, task_description: str, duration: str = "1m") -> str: """ Run a task autonomously in the background, continuously working on a problem and building a final answer. Args: task_description (str): A description of the task to be performed. - interval (str): The interval between each iteration. Format: "". + duration (str): The total duration for the task. Format: "". Units: 's' for seconds, 'm' for minutes, 'h' for hours. - Examples: "30s", "5m", "1h". Default: "30s". - max_iterations (int): The maximum number of iterations to perform. Default: 10. + Examples: "30s", "5m", "1h". Default: "1m". Returns: - str: A message indicating the continuous background task has been started. + response (str): A message indicating the continuous background task has been started. """ - interval_seconds = self._parse_duration(interval) + duration_seconds = self._parse_duration(duration) + interval_seconds = 20 # Enforcing 20-second interval task_id = f"task_{int(time.time())}" + self.pause_flags[task_id] = False + self.latest_results[task_id] = {"current_answer": "", "iterations": 0, "status": "running"} def continuous_background_task(): - iteration = 0 - final_answer = "" - while iteration < max_iterations: - iteration += 1 - self.notify(f"Starting iteration {iteration} of {max_iterations}") - - # Perform analysis and build upon the final answer - analysis = self._perform_analysis(task_description, final_answer) - final_answer += f"\n\nIteration {iteration} result:\n{analysis}" - - self.notify(f"Iteration {iteration} completed. Waiting for {interval} before next iteration.") - time.sleep(interval_seconds) - - self._trigger_apple_dialog("Task Completed", f"Continuous task completed after {max_iterations} iterations.") - self.complete_task(task_id, f"Continuous task '{task_description}' completed. Final answer: {final_answer}") + start_time = time.time() + current_answer = "" + iterations = 0 + + try: + while time.time() - start_time < duration_seconds: + if self.pause_flags[task_id]: + time.sleep(1) + continue + + iterations += 1 + elapsed_time = int(time.time() - start_time) + + prompt = f""" + Task: {task_description} + Current answer: {current_answer} + Iteration: {iterations} + Elapsed time: {elapsed_time} seconds + Total duration: {duration_seconds} seconds + + Please refine the current answer using your critical thinking skills and the task description. + """ + + try: + # Call the LLM to refine the answer + refined_answer = self._call_llm(prompt) + + # Check if we should stop + stop_check = self._check_stop_condition(task_description, refined_answer, iterations, elapsed_time, duration_seconds) + + if stop_check["should_stop"]: + self.notify(f"Task {task_id} stopping. Reason: {stop_check['reasoning']}") + break + + current_answer = refined_answer + self.latest_results[task_id] = { + "current_answer": current_answer, + "iterations": iterations, + "status": "running", + "elapsed_time": elapsed_time + } + + # Write intermediate result + self._write_intermediate_result(task_id, iterations, current_answer) + + except Exception as e: + error_message = f"Error in iteration {iterations}: {str(e)}" + self.notify(error_message) + self._write_intermediate_result(task_id, iterations, f"ERROR: {error_message}") + # Continue to the next iteration instead of breaking the loop + continue + + time.sleep(interval_seconds) + + self.complete_task(task_id, f"Continuous task '{task_description}' completed. Final answer: {current_answer}") + self.latest_results[task_id]["status"] = "completed" + self._trigger_apple_dialog("Task Completed", f"Continuous task completed after {iterations} iterations.") + + except Exception as e: + error_message = f"Critical error in task {task_id}: {str(e)}" + self.notify(error_message) + self._write_intermediate_result(task_id, iterations, f"CRITICAL ERROR: {error_message}") + self.complete_task(task_id, f"Continuous task '{task_description}' failed. Error: {error_message}") + self.latest_results[task_id]["status"] = "failed" + self._trigger_apple_dialog("Task Failed", f"Continuous task failed after {iterations} iterations.") self.autonomous_mode = True self.notify(f"Starting continuous background task: {task_description}") - self.add_task(task_id, task_description, interval_seconds * max_iterations) + self.add_task(task_id, task_description, duration_seconds) - threading.Thread(target=continuous_background_task).start() - return f"Continuous background task '{task_description}' (ID: {task_id}) has been started with {max_iterations} iterations and {interval} interval." + thread = threading.Thread(target=continuous_background_task) + self.loop_threads[task_id] = thread + thread.start() + return f"Continuous background task '{task_description}' (ID: {task_id}) has been started with a duration of {duration}." def _perform_analysis(self, task_description: str, current_answer: str) -> str: """ @@ -168,7 +348,7 @@ def _format_task_status(self, task_id: str, task: dict, current_time: float) -> @tool def get_background_job_status(self) -> str: - """Get the status of all background jobs (ongoing and completed).""" + """Get the status of all background jobs (ongoing, completed, and failed).""" current_time = time.time() status = ["Background Job Status:\n"] @@ -182,6 +362,11 @@ def get_background_job_status(self) -> str: for task in self.completed_tasks: status.append(self._format_task_status(task["description"], task, current_time)) + if self.failed_tasks: + status.append("\nFailed Tasks:") + for task in self.failed_tasks: + status.append(self._format_task_status(task["description"], task, current_time)) + return "\n".join(status) def _create_analysis_prompt(self, analysis_type: str, **kwargs) -> str: @@ -323,7 +508,7 @@ def review_web_page(self, url: str) -> str: self.notifier.status(f"fetching content from {url}") try: web_content = get_web_page_content(url) - self.notifier.status(f"reviewing content...") + self.notifier.status(f"reviewing content: {web_content[:50]}...") return self._ask_ai(f"summarize the following content: {web_content}") except Exception as e: return f"Error: {str(e)}" @@ -431,3 +616,192 @@ def ethical_analysis(self, problem_statement: str, proposed_solution: str) -> st def system_prompt(self) -> str: """Retrieve instructions on how to use this reasoning tool.""" return Message.load("prompts/critical_systems_thinking.jinja").text + + @tool + def pause_autonomous_loop(self, task_id: str) -> str: + """ + Pause the autonomous loop for a given task. + + Args: + task_id (str): The ID of the task to pause. + + Returns: + response (str): A message indicating whether the task was successfully paused. + """ + if task_id in self.pause_flags: + self.pause_flags[task_id] = True + return f"Task {task_id} has been paused." + else: + return f"Task {task_id} not found." + + @tool + def resume_autonomous_loop(self, task_id: str) -> str: + """ + Resume the autonomous loop for a given task. + + Args: + task_id (str): The ID of the task to resume. + + Returns: + response (str): A message indicating whether the task was successfully resumed. + """ + if task_id in self.pause_flags: + self.pause_flags[task_id] = False + return f"Task {task_id} has been resumed." + else: + return f"Task {task_id} not found." + + @tool + def get_latest_results(self, task_id: str) -> str: + """ + Get the latest results for a given task. + + Args: + task_id (str): The ID of the task to get results for. + + Returns: + response (str): A string containing the latest results and iteration count, + or the final result if the task is completed. + """ + if task_id in self.final_results: + return f"Final result for completed task {task_id}:\n{self.final_results[task_id]}" + elif task_id in self.latest_results: + result = self.latest_results[task_id] + return f"Latest results for ongoing task {task_id}:\nIterations: {result['iterations']}\nCurrent answer: {result['current_answer']}" + else: + return f"No results found for task {task_id}." + + @tool + def list_task_ids(self) -> str: + """ + List all task IDs, including ongoing and completed tasks. + + Returns: + response (str): A string containing all task IDs. + """ + all_tasks = list(self.ongoing_tasks.keys()) + [task["description"] for task in self.completed_tasks] + return f"Task IDs:\n" + "\n".join(all_tasks) + + @tool + def get_task_info(self, task_id: str) -> str: + """ + Get detailed information about a specific task. + + Args: + task_id (str): The ID of the task to get information for. + + Returns: + response (str): A string containing detailed information about the task. + """ + if task_id in self.ongoing_tasks: + task = self.ongoing_tasks[task_id] + latest_result = self.latest_results.get(task_id, {}) + return f""" +Task ID: {task_id} +Description: {task['description']} +Status: Ongoing +Start Time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(task['start_time']))} +Elapsed Time: {int(time.time() - task['start_time'])} seconds +Iterations: {latest_result.get('iterations', 'N/A')} +Current Answer: {latest_result.get('current_answer', 'N/A')} +""" + elif task_id in [task["description"] for task in self.completed_tasks]: + task = next(task for task in self.completed_tasks if task["description"] == task_id) + return f""" +Task ID: {task_id} +Description: {task['description']} +Status: Completed +Start Time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(task['start_time']))} +End Time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(task['end_time']))} +Duration: {int(task['end_time'] - task['start_time'])} seconds +Result: {task['result']} +""" + else: + return f"No task found with ID: {task_id}" + + @tool + def interpret_task_query(self, query: str) -> str: + """ + Interpret a natural language query about tasks and provide relevant information. + + Args: + query (str): A natural language query about tasks. + + Returns: + response (str): The interpreted response to the query. + """ + prompt = f""" + Given the following natural language query about tasks, interpret the request and provide the most relevant information: + + Query: {query} + + Consider the following context: + - There may be ongoing, completed, and failed tasks. + - Task IDs are usually in the format "task_timestamp". + - The query might refer to the "last task" or use other relative terms. + - The user might use shorthand or informal language. + - The user might be specifically asking about failed tasks. + + Based on this query, determine: + 1. What specific task(s) is the user asking about? + 2. What information does the user want to know? + 3. Is the user specifically asking about failed tasks? + 4. How should I respond to this query using the available task information? + + Provide your response in the following JSON format: + {{ + "interpreted_task_ids": ["list", "of", "relevant", "task", "ids"], + "requested_info": "brief description of the information requested", + "response_method": "name of the method to use for responding (e.g., 'get_task_info', 'get_latest_results', 'list_task_ids', 'get_background_job_status')", + "additional_context": "any additional context or instructions for formulating the response", + "include_failed_tasks": boolean + }} + """ + + interpretation = self._ask_ai(prompt, include_history=True, system=self.system_prompt()) + + try: + result = json.loads(interpretation) + response = self._generate_task_response(result) + return response + except json.JSONDecodeError: + return f"Error: Unable to interpret the query. Please try rephrasing your question about the tasks." + + def _generate_task_response(self, interpretation: dict) -> str: + """ + Generate a response based on the interpreted task query. + + Args: + interpretation (dict): The interpreted query information. + + Returns: + response (str): The generated response to the query. + """ + response = f"Interpreted request: {interpretation['requested_info']}\n\n" + + if interpretation['response_method'] == 'list_task_ids': + response += self.list_task_ids() + elif interpretation['response_method'] == 'get_background_job_status': + response += self.get_background_job_status() + elif interpretation['response_method'] in ['get_task_info', 'get_latest_results']: + for task_id in interpretation['interpreted_task_ids']: + if interpretation['response_method'] == 'get_task_info': + response += self.get_task_info(task_id) + "\n" + else: + response += self.get_latest_results(task_id) + "\n" + else: + response += f"Unable to process the request using method: {interpretation['response_method']}" + + if interpretation.get('include_failed_tasks', False): + failed_tasks = [task for task in self.failed_tasks] + if failed_tasks: + response += "\nFailed Tasks:\n" + for task in failed_tasks: + response += f"- Task ID: {task['description']}\n Result: {task['result']}\n" + else: + response += "\nNo failed tasks found." + + if interpretation['additional_context']: + response += f"\nAdditional context: {interpretation['additional_context']}" + + return response diff --git a/src/goose_plugins/utils/log_streamer.py b/src/goose_plugins/utils/log_streamer.py new file mode 100644 index 0000000..f54b825 --- /dev/null +++ b/src/goose_plugins/utils/log_streamer.py @@ -0,0 +1,26 @@ +import asyncio +import aiofiles +import json +from pathlib import Path + +LOG_FILE = Path.home() / ".goose" / "session_log.json" + +async def stream_log(): + if not LOG_FILE.exists(): + print("No active Goose session log found.") + return + + async with aiofiles.open(LOG_FILE, mode='r') as f: + while True: + line = await f.readline() + if not line: + await asyncio.sleep(0.1) + continue + try: + log_entry = json.loads(line) + print(f"{log_entry['timestamp']} - {log_entry['action']}: {log_entry['details']}") + except json.JSONDecodeError: + print(f"Error decoding log entry: {line}") + +def start_log_stream(): + asyncio.run(stream_log()) \ No newline at end of file From 6c9f8eadda6a5982630d0f722786c481d5355ffd Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 15:44:16 -0400 Subject: [PATCH 17/22] Clean up code. --- .../toolkits/critical_systems_thinking.py | 697 ++---------------- .../toolkits/tools/analysis_tools.py | 136 ++++ .../toolkits/tools/search_tools.py | 37 + .../toolkits/tools/task_management_tools.py | 324 ++++++++ .../toolkits/utils/analysis_utils.py | 77 ++ .../toolkits/utils/logging_utils.py | 31 + .../toolkits/utils/task_utils.py | 40 + .../test_critical_systems_thinking.py | 8 +- 8 files changed, 702 insertions(+), 648 deletions(-) create mode 100644 src/goose_plugins/toolkits/tools/analysis_tools.py create mode 100644 src/goose_plugins/toolkits/tools/search_tools.py create mode 100644 src/goose_plugins/toolkits/tools/task_management_tools.py create mode 100644 src/goose_plugins/toolkits/utils/analysis_utils.py create mode 100644 src/goose_plugins/toolkits/utils/logging_utils.py create mode 100644 src/goose_plugins/toolkits/utils/task_utils.py diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index 42ce3d5..e331dd9 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -1,20 +1,25 @@ from exchange import Exchange, Message, Text from exchange.content import Content from exchange.providers import AnthropicProvider -from goose.toolkit.base import Toolkit, tool +from goose.toolkit.base import Toolkit from goose.utils.ask import ask_an_ai -from goose_plugins.utils.selenium_web_browser import get_web_page_content -from goose_plugins.utils.serper_search import serper_search import queue import time import threading import os -import json -from pathlib import Path import signal import atexit +import sys +from pathlib import Path +import json -class CriticalSystemsThinking(Toolkit): +from .utils.task_utils import parse_duration, format_task_status, write_intermediate_result, trigger_apple_dialog +from .utils.analysis_utils import create_analysis_prompt +from .tools.search_tools import SearchTools +from .tools.analysis_tools import AnalysisTools +from .tools.task_management_tools import TaskManagementTools + +class CriticalSystemsThinking(Toolkit, SearchTools, AnalysisTools, TaskManagementTools): """Critical systems thinking toolkit for understanding and solving complex problems.""" def __init__(self, *args, **kwargs): @@ -32,12 +37,21 @@ def __init__(self, *args, **kwargs): os.makedirs(self.results_folder, exist_ok=True) self.log_file = Path.home() / ".goose" / "session_log.json" self.log_file.parent.mkdir(parents=True, exist_ok=True) + self.tool_log_file = Path.home() / ".goose" / "tool_calls.log" + self.tool_log_file.parent.mkdir(parents=True, exist_ok=True) # Set up signal handler for graceful shutdown signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler) atexit.register(self._cleanup) + def log_tool_call(self, tool_name: str, parameters: dict): + """Log a tool call to the tool_calls.log file.""" + timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + log_entry = f"{timestamp} - Tool: {tool_name}, Parameters: {json.dumps(parameters)}\n" + with open(self.tool_log_file, "a") as log: + log.write(log_entry) + def _signal_handler(self, signum, frame): """Handle termination signals.""" self.notify("Received termination signal. Cleaning up...") @@ -45,29 +59,25 @@ def _signal_handler(self, signum, frame): sys.exit(0) def _cleanup(self): - """Clean up all running tasks.""" + """Clean up all running tasks and terminate threads.""" self.notify("Cleaning up and aborting all tasks...") for task_id in list(self.ongoing_tasks.keys()): self._abort_task(task_id) - self.notify("All tasks aborted.") - - async def log_action(self, action: str, details: str): - """Log an action to the session log file.""" - log_entry = { - "timestamp": time.time(), - "action": action, - "details": details - } - - def _write_intermediate_result(self, task_id: str, iteration: int, result: str): - """Write intermediate results to a file for debugging purposes.""" - filename = f"{task_id}_iteration_{iteration}.txt" - filepath = os.path.join(self.results_folder, filename) - with open(filepath, 'w') as f: - f.write(f"Task ID: {task_id}\n") - f.write(f"Iteration: {iteration}\n") - f.write(f"Result:\n{result}\n") - self.notify(f"Intermediate result written to {filepath}") + + # Terminate all threads + for thread in self.loop_threads.values(): + if thread.is_alive(): + thread.join(timeout=1) # Give threads 1 second to finish + + # Force terminate any remaining threads + for thread in threading.enumerate(): + if thread != threading.main_thread(): + try: + thread._stop() + except: + pass + + self.notify("All tasks aborted and threads terminated.") def _create_exchange(self, include_history=False, system=None) -> Exchange: """Create a new Exchange instance with optional message history.""" @@ -100,6 +110,11 @@ def notify(self, message: str): def add_task(self, task_id: str, task_description: str, duration: int): """Add a task to the ongoing tasks dictionary.""" + self.log_tool_call("add_task", { + "task_id": task_id, + "task_description": task_description[:50] + "...", + "duration": duration + }) self.ongoing_tasks[task_id] = { "description": task_description, "start_time": time.time(), @@ -126,27 +141,6 @@ def _abort_task(self, task_id: str): if task_id in self.loop_threads: self.loop_threads[task_id].join(timeout=1) # Wait for the thread to finish self.complete_task(task_id, "Task aborted", success=False) - self.notify(f"Task {task_id} aborted.") - else: - self.notify(f"Task {task_id} not found or already completed.") - - def notify_user(self, message: str): - """Notify the user when help is needed.""" - print(f"[User Notification] {message}") - - def _parse_duration(self, duration: str) -> int: - """Convert duration string to seconds.""" - value = int(duration[:-1]) - unit = duration[-1].lower() - multipliers = {'s': 1, 'm': 60, 'h': 3600} - if unit not in multipliers: - raise ValueError("Invalid duration format. Use 's' for seconds, 'm' for minutes, or 'h' for hours.") - return value * multipliers[unit] - - def _trigger_apple_dialog(self, title: str, message: str): - """Trigger an Apple dialog box.""" - dialog_command = f'display dialog "{message}" buttons {{"OK"}} default button "OK" with title "{title}"' - os.system(f"osascript -e '{dialog_command}'") def _call_llm(self, prompt: str) -> str: """ @@ -158,9 +152,11 @@ def _call_llm(self, prompt: str) -> str: Returns: response (str): The refined answer from the language model. """ + self.log_tool_call("_call_llm", {"prompt": prompt[:50] + "..."}) # Log first 50 chars of prompt return self._ask_ai(prompt, include_history=True, system=self.system_prompt()) - def _check_stop_condition(self, task_description: str, current_answer: str, iterations: int, elapsed_time: int, total_duration: int) -> dict: + def _check_stop_condition(self, task_description: str, current_answer: str, + iterations: int, elapsed_time: int, total_duration: int) -> dict: """ Check if the autonomous loop should stop based on the current state of the task. @@ -174,6 +170,14 @@ def _check_stop_condition(self, task_description: str, current_answer: str, iter Returns: dict: A dictionary containing the stop decision and reasoning. """ + self.log_tool_call("_check_stop_condition", { + "task_description": task_description[:50] + "...", + "current_answer": current_answer[:50] + "...", + "iterations": iterations, + "elapsed_time": elapsed_time, + "total_duration": total_duration + }) + prompt = f""" Task: {task_description} Current answer: {current_answer} @@ -193,7 +197,6 @@ def _check_stop_condition(self, task_description: str, current_answer: str, iter """ exchange = self._create_exchange(include_history=True, system=self.system_prompt()) - # exchange.add(Message(role="user", content=[Text(prompt)])) response = ask_an_ai(input=prompt, exchange=exchange) try: @@ -208,600 +211,6 @@ def _check_stop_condition(self, task_description: str, current_answer: str, iter self.notify(f"Error: {str(e)}") return {"should_stop": False, "reasoning": "Error in LLM response structure. Continuing task."} - @tool - def autonomous_loop(self, task_description: str, duration: str = "1m") -> str: - """ - Run a task autonomously in the background, continuously working on a problem and building a final answer. - - Args: - task_description (str): A description of the task to be performed. - duration (str): The total duration for the task. Format: "". - Units: 's' for seconds, 'm' for minutes, 'h' for hours. - Examples: "30s", "5m", "1h". Default: "1m". - - Returns: - response (str): A message indicating the continuous background task has been started. - """ - duration_seconds = self._parse_duration(duration) - interval_seconds = 20 # Enforcing 20-second interval - task_id = f"task_{int(time.time())}" - self.pause_flags[task_id] = False - self.latest_results[task_id] = {"current_answer": "", "iterations": 0, "status": "running"} - - def continuous_background_task(): - start_time = time.time() - current_answer = "" - iterations = 0 - - try: - while time.time() - start_time < duration_seconds: - if self.pause_flags[task_id]: - time.sleep(1) - continue - - iterations += 1 - elapsed_time = int(time.time() - start_time) - - prompt = f""" - Task: {task_description} - Current answer: {current_answer} - Iteration: {iterations} - Elapsed time: {elapsed_time} seconds - Total duration: {duration_seconds} seconds - - Please refine the current answer using your critical thinking skills and the task description. - """ - - try: - # Call the LLM to refine the answer - refined_answer = self._call_llm(prompt) - - # Check if we should stop - stop_check = self._check_stop_condition(task_description, refined_answer, iterations, elapsed_time, duration_seconds) - - if stop_check["should_stop"]: - self.notify(f"Task {task_id} stopping. Reason: {stop_check['reasoning']}") - break - - current_answer = refined_answer - self.latest_results[task_id] = { - "current_answer": current_answer, - "iterations": iterations, - "status": "running", - "elapsed_time": elapsed_time - } - - # Write intermediate result - self._write_intermediate_result(task_id, iterations, current_answer) - - except Exception as e: - error_message = f"Error in iteration {iterations}: {str(e)}" - self.notify(error_message) - self._write_intermediate_result(task_id, iterations, f"ERROR: {error_message}") - # Continue to the next iteration instead of breaking the loop - continue - - time.sleep(interval_seconds) - - self.complete_task(task_id, f"Continuous task '{task_description}' completed. Final answer: {current_answer}") - self.latest_results[task_id]["status"] = "completed" - self._trigger_apple_dialog("Task Completed", f"Continuous task completed after {iterations} iterations.") - - except Exception as e: - error_message = f"Critical error in task {task_id}: {str(e)}" - self.notify(error_message) - self._write_intermediate_result(task_id, iterations, f"CRITICAL ERROR: {error_message}") - self.complete_task(task_id, f"Continuous task '{task_description}' failed. Error: {error_message}") - self.latest_results[task_id]["status"] = "failed" - self._trigger_apple_dialog("Task Failed", f"Continuous task failed after {iterations} iterations.") - - self.autonomous_mode = True - self.notify(f"Starting continuous background task: {task_description}") - self.add_task(task_id, task_description, duration_seconds) - - thread = threading.Thread(target=continuous_background_task) - self.loop_threads[task_id] = thread - thread.start() - return f"Continuous background task '{task_description}' (ID: {task_id}) has been started with a duration of {duration}." - - def _perform_analysis(self, task_description: str, current_answer: str) -> str: - """ - Perform analysis for a single iteration of the continuous background task. - This method can invoke other tools as needed. - """ - prompt = f""" - Task description: {task_description} - Current progress: {current_answer} - - Analyze the current progress and provide the next step or insight for solving the problem. - You can use any of the available tools to gather more information or perform specific analyses. - Return your analysis and any tool results in a concise manner. - """ - - analysis = self._ask_ai(prompt, include_history=True) - - # Example of invoking other tools within the analysis - if "need more information" in analysis.lower(): - search_query = f"latest information about {task_description}" - search_results = self.search(search_query) - analysis += f"\n\nAdditional information from search:\n{search_results}" - - if "stakeholder analysis needed" in analysis.lower(): - stakeholder_analysis = self.stakeholder_analysis(task_description) - analysis += f"\n\nStakeholder analysis results:\n{stakeholder_analysis}" - - return analysis - - def _format_task_status(self, task_id: str, task: dict, current_time: float) -> str: - """Format a single task's status.""" - elapsed_time = current_time - task["start_time"] - remaining_time = max(0, task.get("duration", 0) - elapsed_time) - status = f"- Task ID: {task_id}\n" - status += f" Description: {task['description']}\n" - if "end_time" in task: - status += f" Duration: {task['end_time'] - task['start_time']:.2f} seconds\n" - status += f" Result: {task['result']}\n" - else: - status += f" Elapsed Time: {elapsed_time:.2f} seconds\n" - status += f" Remaining Time: {remaining_time:.2f} seconds\n" - return status - - @tool - def get_background_job_status(self) -> str: - """Get the status of all background jobs (ongoing, completed, and failed).""" - current_time = time.time() - status = ["Background Job Status:\n"] - - if self.ongoing_tasks: - status.append("\nOngoing Tasks:") - for task_id, task in self.ongoing_tasks.items(): - status.append(self._format_task_status(task_id, task, current_time)) - - if self.completed_tasks: - status.append("\nCompleted Tasks:") - for task in self.completed_tasks: - status.append(self._format_task_status(task["description"], task, current_time)) - - if self.failed_tasks: - status.append("\nFailed Tasks:") - for task in self.failed_tasks: - status.append(self._format_task_status(task["description"], task, current_time)) - - return "\n".join(status) - - def _create_analysis_prompt(self, analysis_type: str, **kwargs) -> str: - """Create a prompt for various types of analysis.""" - prompts = { - "structured": lambda p: f""" - Perform a structured analysis of the following problem using the MECE principle: - {p} - Return the results as a JSON string with the following structure: - {{ - "problem": "Brief restatement of the problem", - "categories": [ - {{ - "name": "Category Name", - "elements": ["Element 1", "Element 2", ...], - "analysis": "Brief analysis of this category" - }}, - ... - ], - "conclusion": "Overall conclusion based on the structured analysis" - }} - """, - "request": lambda s: f""" - Analyze the user statement: {s} - If you need to immediately clarify something and it's something - short and simple, respond with your question(s). - If you need multiple questions, you can ask multiple questions. - Please bullet point your questions. - Limit your response to 5 questions. - """, - "solutions": lambda s: f""" - Analyze the user statement: {s} - Consider the existing message history and provide a well thought out response. - Provide one or more potential solutions to the problem. - Limit your response to 5 solutions. - """, - "stakeholder": lambda p: f""" - Analyze the following problem statement and identify key stakeholders: - {p} - For each stakeholder, determine their interests and potential impacts. - """, - "future_scenarios": lambda p, t: f""" - Based on the following problem statement and time horizon, generate potential future scenarios: - Problem: {p} - Time Horizon: {t} - - Consider various factors such as technological advancements, societal changes, - environmental impacts, and potential policy shifts. - - Return the results as a JSON string with scenarios including name, description, - key factors, and potential outcomes. - Generate at least 3 distinct scenarios. - """, - "system_mapping": lambda p: f""" - Based on the following problem statement, create a high-level system map: - {p} - - Identify key components, their relationships, and potential feedback loops. - Return results as JSON with components and feedback loops. - """, - "risk": lambda p, s: f""" - Perform a risk assessment for the following problem and proposed solution: - Problem: {p} - Proposed Solution: {s} - - Identify potential risks, their likelihood, impact, and possible mitigation strategies. - Return results as JSON with detailed risk assessments. - """, - "ethical": lambda p, s: f""" - Perform an ethical analysis for the following problem and proposed solution: - Problem: {p} - Proposed Solution: {s} - - Consider various ethical frameworks and principles, potential ethical dilemmas, - and the impact on different stakeholders. - Return results as JSON with ethical considerations and overall assessment. - """ - } - return prompts[analysis_type](**kwargs) - - @tool - def structured_analysis(self, problem: str) -> str: - """ - Perform a structured analysis of the given problem using the MECE principle. - - Args: - problem (str): A description of the problem to analyze. - - Returns: - response (str): A JSON string containing the structured analysis. - """ - self.notify("Performing structured analysis") - return self._ask_ai(self._create_analysis_prompt("structured", p=problem)) - - @tool - def search(self, query: str) -> str: - """ - Search the web for information using the Serper API. - - Args: - query (str): query to search for. - - Returns: - response (str): A JSON response containing search results. - """ - self.notifier.status("searching...") - return serper_search(query) - - @tool - def analyze_request(self, statement: str) -> str: - """ - When a request is unclear, high-level or ambiguous use this tool to - analyze the response and provide a well thought out response. - - Args: - statement (str): description of problem or errors seen. - - Returns: - response (str): A well thought out response to the statement or question. - """ - self.notifier.status("analyzing request...") - return self._ask_ai( - self._create_analysis_prompt("request", s=statement), - include_history=True, - system=self.system_prompt() - ) - - @tool - def review_web_page(self, url: str) -> str: - """ - Review the content of a web page by providing a summary of the content. - - Args: - url (str): URL of the web page to review. - - Returns: - response (str): A summary of the content of the web page. - """ - self.notifier.status(f"fetching content from {url}") - try: - web_content = get_web_page_content(url) - self.notifier.status(f"reviewing content: {web_content[:50]}...") - return self._ask_ai(f"summarize the following content: {web_content}") - except Exception as e: - return f"Error: {str(e)}" - - @tool - def consider_solutions(self, statement: str) -> str: - """ - Provide a well thought out response to the statement summarize the - problem and provide a solution or a set of solutions. - - Args: - statement (str): description of problem or errors seen. - - Returns: - response (str): A well thought out response to the statement or question. - """ - self.notifier.status("considering solutions...") - return self._ask_ai( - self._create_analysis_prompt("solutions", s=statement), - include_history=True - ) - - @tool - def stakeholder_analysis(self, problem_statement: str) -> str: - """ - Identify and analyze key stakeholders related to the given problem. - - Args: - problem_statement (str): A description of the problem or situation. - - Returns: - response (str): A JSON string containing a list of stakeholders, their interests, and potential impacts. - """ - self.notifier.status("Analyzing stakeholders...") - return self._ask_ai( - self._create_analysis_prompt("stakeholder", p=problem_statement), - include_history=True - ) - - @tool - def generate_future_scenarios(self, problem_statement: str, time_horizon: str) -> str: - """ - Generate potential future scenarios based on the given problem statement and time horizon. - - Args: - problem_statement (str): A description of the current problem or situation. - time_horizon (str): The future time frame to consider (e.g., "5 years", "10 years", "50 years"). - - Returns: - response (str): A JSON string containing a list of potential future scenarios. - """ - self.notifier.status("Generating future scenarios...") - return self._ask_ai( - self._create_analysis_prompt("future_scenarios", p=problem_statement, t=time_horizon) - ) - - @tool - def system_mapping(self, problem_statement: str) -> str: - """ - Create a high-level system map based on the given problem statement. - - Args: - problem_statement (str): A description of the problem or situation. - - Returns: - response (str): A JSON string representing a high-level system map. - """ - self.notifier.status("Creating system map...") - return self._ask_ai(self._create_analysis_prompt("system_mapping", p=problem_statement)) - - @tool - def risk_assessment(self, problem_statement: str, proposed_solution: str) -> str: - """ - Perform a risk assessment for the given problem and proposed solution. - - Args: - problem_statement (str): A description of the problem or situation. - proposed_solution (str): A description of the proposed solution. - - Returns: - response (str): A JSON string containing a list of potential risks and their assessments. - """ - self.notifier.status("Performing risk assessment...") - return self._ask_ai( - self._create_analysis_prompt("risk", p=problem_statement, s=proposed_solution) - ) - - @tool - def ethical_analysis(self, problem_statement: str, proposed_solution: str) -> str: - """ - Perform an ethical analysis of the given problem and proposed solution. - - Args: - problem_statement (str): A description of the problem or situation. - proposed_solution (str): A description of the proposed solution. - - Returns: - response (str): A JSON string containing an ethical analysis of the problem and solution. - """ - self.notifier.status("Performing ethical analysis...") - return self._ask_ai( - self._create_analysis_prompt("ethical", p=problem_statement, s=proposed_solution) - ) - def system_prompt(self) -> str: """Retrieve instructions on how to use this reasoning tool.""" - return Message.load("prompts/critical_systems_thinking.jinja").text - - @tool - def pause_autonomous_loop(self, task_id: str) -> str: - """ - Pause the autonomous loop for a given task. - - Args: - task_id (str): The ID of the task to pause. - - Returns: - response (str): A message indicating whether the task was successfully paused. - """ - if task_id in self.pause_flags: - self.pause_flags[task_id] = True - return f"Task {task_id} has been paused." - else: - return f"Task {task_id} not found." - - @tool - def resume_autonomous_loop(self, task_id: str) -> str: - """ - Resume the autonomous loop for a given task. - - Args: - task_id (str): The ID of the task to resume. - - Returns: - response (str): A message indicating whether the task was successfully resumed. - """ - if task_id in self.pause_flags: - self.pause_flags[task_id] = False - return f"Task {task_id} has been resumed." - else: - return f"Task {task_id} not found." - - @tool - def get_latest_results(self, task_id: str) -> str: - """ - Get the latest results for a given task. - - Args: - task_id (str): The ID of the task to get results for. - - Returns: - response (str): A string containing the latest results and iteration count, - or the final result if the task is completed. - """ - if task_id in self.final_results: - return f"Final result for completed task {task_id}:\n{self.final_results[task_id]}" - elif task_id in self.latest_results: - result = self.latest_results[task_id] - return f"Latest results for ongoing task {task_id}:\nIterations: {result['iterations']}\nCurrent answer: {result['current_answer']}" - else: - return f"No results found for task {task_id}." - - @tool - def list_task_ids(self) -> str: - """ - List all task IDs, including ongoing and completed tasks. - - Returns: - response (str): A string containing all task IDs. - """ - all_tasks = list(self.ongoing_tasks.keys()) + [task["description"] for task in self.completed_tasks] - return f"Task IDs:\n" + "\n".join(all_tasks) - - @tool - def get_task_info(self, task_id: str) -> str: - """ - Get detailed information about a specific task. - - Args: - task_id (str): The ID of the task to get information for. - - Returns: - response (str): A string containing detailed information about the task. - """ - if task_id in self.ongoing_tasks: - task = self.ongoing_tasks[task_id] - latest_result = self.latest_results.get(task_id, {}) - return f""" -Task ID: {task_id} -Description: {task['description']} -Status: Ongoing -Start Time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(task['start_time']))} -Elapsed Time: {int(time.time() - task['start_time'])} seconds -Iterations: {latest_result.get('iterations', 'N/A')} -Current Answer: {latest_result.get('current_answer', 'N/A')} -""" - elif task_id in [task["description"] for task in self.completed_tasks]: - task = next(task for task in self.completed_tasks if task["description"] == task_id) - return f""" -Task ID: {task_id} -Description: {task['description']} -Status: Completed -Start Time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(task['start_time']))} -End Time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(task['end_time']))} -Duration: {int(task['end_time'] - task['start_time'])} seconds -Result: {task['result']} -""" - else: - return f"No task found with ID: {task_id}" - - @tool - def interpret_task_query(self, query: str) -> str: - """ - Interpret a natural language query about tasks and provide relevant information. - - Args: - query (str): A natural language query about tasks. - - Returns: - response (str): The interpreted response to the query. - """ - prompt = f""" - Given the following natural language query about tasks, interpret the request and provide the most relevant information: - - Query: {query} - - Consider the following context: - - There may be ongoing, completed, and failed tasks. - - Task IDs are usually in the format "task_timestamp". - - The query might refer to the "last task" or use other relative terms. - - The user might use shorthand or informal language. - - The user might be specifically asking about failed tasks. - - Based on this query, determine: - 1. What specific task(s) is the user asking about? - 2. What information does the user want to know? - 3. Is the user specifically asking about failed tasks? - 4. How should I respond to this query using the available task information? - - Provide your response in the following JSON format: - {{ - "interpreted_task_ids": ["list", "of", "relevant", "task", "ids"], - "requested_info": "brief description of the information requested", - "response_method": "name of the method to use for responding (e.g., 'get_task_info', 'get_latest_results', 'list_task_ids', 'get_background_job_status')", - "additional_context": "any additional context or instructions for formulating the response", - "include_failed_tasks": boolean - }} - """ - - interpretation = self._ask_ai(prompt, include_history=True, system=self.system_prompt()) - - try: - result = json.loads(interpretation) - response = self._generate_task_response(result) - return response - except json.JSONDecodeError: - return f"Error: Unable to interpret the query. Please try rephrasing your question about the tasks." - - def _generate_task_response(self, interpretation: dict) -> str: - """ - Generate a response based on the interpreted task query. - - Args: - interpretation (dict): The interpreted query information. - - Returns: - response (str): The generated response to the query. - """ - response = f"Interpreted request: {interpretation['requested_info']}\n\n" - - if interpretation['response_method'] == 'list_task_ids': - response += self.list_task_ids() - elif interpretation['response_method'] == 'get_background_job_status': - response += self.get_background_job_status() - elif interpretation['response_method'] in ['get_task_info', 'get_latest_results']: - for task_id in interpretation['interpreted_task_ids']: - if interpretation['response_method'] == 'get_task_info': - response += self.get_task_info(task_id) + "\n" - else: - response += self.get_latest_results(task_id) + "\n" - else: - response += f"Unable to process the request using method: {interpretation['response_method']}" - - if interpretation.get('include_failed_tasks', False): - failed_tasks = [task for task in self.failed_tasks] - if failed_tasks: - response += "\nFailed Tasks:\n" - for task in failed_tasks: - response += f"- Task ID: {task['description']}\n Result: {task['result']}\n" - else: - response += "\nNo failed tasks found." - - if interpretation['additional_context']: - response += f"\nAdditional context: {interpretation['additional_context']}" - - return response + return Message.load("prompts/critical_systems_thinking.jinja").text \ No newline at end of file diff --git a/src/goose_plugins/toolkits/tools/analysis_tools.py b/src/goose_plugins/toolkits/tools/analysis_tools.py new file mode 100644 index 0000000..c2b03b0 --- /dev/null +++ b/src/goose_plugins/toolkits/tools/analysis_tools.py @@ -0,0 +1,136 @@ +from goose.toolkit.base import tool +from ..utils.analysis_utils import create_analysis_prompt + +class AnalysisTools: + @tool + def structured_analysis(self, problem: str) -> str: + """ + Perform a structured analysis of the given problem using the MECE principle. + + Args: + problem (str): A description of the problem to analyze. + + Returns: + response (str): A JSON string containing the structured analysis. + """ + self.notify("Performing structured analysis") + return self._ask_ai(create_analysis_prompt("structured", p=problem)) + + @tool + def analyze_request(self, statement: str) -> str: + """ + When a request is unclear, high-level or ambiguous use this tool to + analyze the response and provide a well thought out response. + + Args: + statement (str): description of problem or errors seen. + + Returns: + response (str): A well thought out response to the statement or question. + """ + self.notifier.status("analyzing request...") + return self._ask_ai( + create_analysis_prompt("request", s=statement), + include_history=True, + system=self.system_prompt() + ) + + @tool + def consider_solutions(self, statement: str) -> str: + """ + Provide a well thought out response to the statement summarize the + problem and provide a solution or a set of solutions. + + Args: + statement (str): description of problem or errors seen. + + Returns: + response (str): A well thought out response to the statement or question. + """ + self.notifier.status("considering solutions...") + return self._ask_ai( + create_analysis_prompt("solutions", s=statement), + include_history=True + ) + + @tool + def stakeholder_analysis(self, problem_statement: str) -> str: + """ + Identify and analyze key stakeholders related to the given problem. + + Args: + problem_statement (str): A description of the problem or situation. + + Returns: + response (str): A JSON string containing a list of stakeholders, their interests, and potential impacts. + """ + self.notifier.status("Analyzing stakeholders...") + return self._ask_ai( + create_analysis_prompt("stakeholder", p=problem_statement), + include_history=True + ) + + @tool + def generate_future_scenarios(self, problem_statement: str, time_horizon: str) -> str: + """ + Generate potential future scenarios based on the given problem statement and time horizon. + + Args: + problem_statement (str): A description of the current problem or situation. + time_horizon (str): The future time frame to consider (e.g., "5 years", "10 years", "50 years"). + + Returns: + response (str): A JSON string containing a list of potential future scenarios. + """ + self.notifier.status("Generating future scenarios...") + return self._ask_ai( + create_analysis_prompt("future_scenarios", p=problem_statement, t=time_horizon) + ) + + @tool + def system_mapping(self, problem_statement: str) -> str: + """ + Create a high-level system map based on the given problem statement. + + Args: + problem_statement (str): A description of the problem or situation. + + Returns: + response (str): A JSON string representing a high-level system map. + """ + self.notifier.status("Creating system map...") + return self._ask_ai(create_analysis_prompt("system_mapping", p=problem_statement)) + + @tool + def risk_assessment(self, problem_statement: str, proposed_solution: str) -> str: + """ + Perform a risk assessment for the given problem and proposed solution. + + Args: + problem_statement (str): A description of the problem or situation. + proposed_solution (str): A description of the proposed solution. + + Returns: + response (str): A JSON string containing a list of potential risks and their assessments. + """ + self.notifier.status("Performing risk assessment...") + return self._ask_ai( + create_analysis_prompt("risk", p=problem_statement, s=proposed_solution) + ) + + @tool + def ethical_analysis(self, problem_statement: str, proposed_solution: str) -> str: + """ + Perform an ethical analysis of the given problem and proposed solution. + + Args: + problem_statement (str): A description of the problem or situation. + proposed_solution (str): A description of the proposed solution. + + Returns: + response (str): A JSON string containing an ethical analysis of the problem and solution. + """ + self.notifier.status("Performing ethical analysis...") + return self._ask_ai( + create_analysis_prompt("ethical", p=problem_statement, s=proposed_solution) + ) \ No newline at end of file diff --git a/src/goose_plugins/toolkits/tools/search_tools.py b/src/goose_plugins/toolkits/tools/search_tools.py new file mode 100644 index 0000000..29b5a3a --- /dev/null +++ b/src/goose_plugins/toolkits/tools/search_tools.py @@ -0,0 +1,37 @@ +from goose.toolkit.base import tool +from goose_plugins.utils.serper_search import serper_search +from goose_plugins.utils.selenium_web_browser import get_web_page_content + +class SearchTools: + @tool + def search(self, query: str) -> str: + """ + Search the web for information using the Serper API. + + Args: + query (str): query to search for. + + Returns: + response (str): A JSON response containing search results. + """ + self.notifier.status("searching...") + return serper_search(query) + + @tool + def review_web_page(self, url: str) -> str: + """ + Review the content of a web page by providing a summary of the content. + + Args: + url (str): URL of the web page to review. + + Returns: + response (str): A summary of the content of the web page. + """ + self.notifier.status(f"fetching content from {url}") + try: + web_content = get_web_page_content(url) + self.notifier.status(f"reviewing content: {web_content[:50]}...") + return self._ask_ai(f"summarize the following content: {web_content}") + except Exception as e: + return f"Error: {str(e)}" \ No newline at end of file diff --git a/src/goose_plugins/toolkits/tools/task_management_tools.py b/src/goose_plugins/toolkits/tools/task_management_tools.py new file mode 100644 index 0000000..b5d2d0f --- /dev/null +++ b/src/goose_plugins/toolkits/tools/task_management_tools.py @@ -0,0 +1,324 @@ +from goose.toolkit.base import tool +import json +import time +import threading +from ..utils.task_utils import format_task_status, parse_duration, write_intermediate_result, trigger_apple_dialog + +class TaskManagementTools: + @tool + def autonomous_loop(self, task_description: str, duration: str = "1m") -> str: + """ + Run a task autonomously in the background, continuously working on a problem and building a final answer. + + Args: + task_description (str): A description of the task to be performed. + duration (str): The total duration for the task. Format: "". + Units: 's' for seconds, 'm' for minutes, 'h' for hours. + Examples: "30s", "5m", "1h". Default: "1m". + + Returns: + response (str): A message indicating the continuous background task has been started. + """ + duration_seconds = parse_duration(duration) + interval_seconds = 20 # Enforcing 20-second interval + task_id = f"task_{int(time.time())}" + self.pause_flags[task_id] = False + self.latest_results[task_id] = {"current_answer": "", "iterations": 0, "status": "running"} + self.notifier.status(f"Starting autonomous loop for task {task_id}: {task_description}") + + def continuous_background_task(): + start_time = time.time() + current_answer = "" + iterations = 0 + + # Initialize the task in latest_results + self.latest_results[task_id] = { + "current_answer": current_answer, + "iterations": iterations, + "status": "running", + "elapsed_time": 0 + } + + try: + while time.time() - start_time < duration_seconds: + if self.pause_flags[task_id]: + time.sleep(1) + continue + + iterations += 1 + elapsed_time = int(time.time() - start_time) + + prompt = f""" + Task: {task_description} + Current answer: {current_answer} + Iteration: {iterations} + Elapsed time: {elapsed_time} seconds + Total duration: {duration_seconds} seconds + + Please refine the current answer using your critical thinking skills and the task description. + """ + + try: + # Call the LLM to refine the answer + refined_answer = self._call_llm(prompt) + + # Check if we should stop + stop_check = self._check_stop_condition(task_description, refined_answer, iterations, elapsed_time, duration_seconds) + + if stop_check["should_stop"]: + break + + current_answer = refined_answer + self.latest_results[task_id].update({ + "current_answer": current_answer, + "iterations": iterations, + "status": "running", + "elapsed_time": elapsed_time + }) + + # Write intermediate result + write_intermediate_result(self.results_folder, task_id, iterations, current_answer) + + except Exception as e: + error_message = f"Error in iteration {iterations}: {str(e)}" + # self.logger.error(f"Task {task_id}: {error_message}") + write_intermediate_result(self.results_folder, task_id, iterations, f"ERROR: {error_message}") + # Continue to the next iteration instead of breaking the loop + continue + + time.sleep(interval_seconds) + + self.complete_task(task_id, f"Continuous task '{task_description}' completed. Final answer: {current_answer}") + if task_id in self.latest_results: + self.latest_results[task_id]["status"] = "completed" + trigger_apple_dialog("Task Completed", f"Continuous task completed after {iterations} iterations.") + + except Exception as e: + error_message = f"Critical error in task {task_id}: {str(e)}" + self.notify(error_message) + write_intermediate_result(self.results_folder, task_id, iterations, f"CRITICAL ERROR: {error_message}") + self.complete_task(task_id, f"Continuous task '{task_description}' failed. Error: {error_message}") + if task_id in self.latest_results: + self.latest_results[task_id]["status"] = "failed" + trigger_apple_dialog("Task Failed", f"Continuous task failed after {iterations} iterations.") + + self.autonomous_mode = True + self.notify(f"Starting continuous background task: {task_description}") + self.add_task(task_id, task_description, duration_seconds) + + thread = threading.Thread(target=continuous_background_task) + self.loop_threads[task_id] = thread + thread.start() + return f"Continuous background task '{task_description}' (ID: {task_id}) has been started with a duration of {duration}." + + @tool + def get_background_job_status(self) -> str: + """Get the status of all background jobs (ongoing, completed, and failed).""" + current_time = time.time() + status = ["Background Job Status:\n"] + + if self.ongoing_tasks: + status.append("\nOngoing Tasks:") + for task_id, task in self.ongoing_tasks.items(): + status.append(format_task_status(task_id, task, current_time)) + + if self.completed_tasks: + status.append("\nCompleted Tasks:") + for task in self.completed_tasks: + status.append(format_task_status(task["description"], task, current_time)) + + if self.failed_tasks: + status.append("\nFailed Tasks:") + for task in self.failed_tasks: + status.append(format_task_status(task["description"], task, current_time)) + + return "\n".join(status) + + @tool + def pause_autonomous_loop(self, task_id: str) -> str: + """ + Pause the autonomous loop for a given task. + + Args: + task_id (str): The ID of the task to pause. + + Returns: + response (str): A message indicating whether the task was successfully paused. + """ + if task_id in self.pause_flags: + self.pause_flags[task_id] = True + return f"Task {task_id} has been paused." + else: + return f"Task {task_id} not found." + + @tool + def resume_autonomous_loop(self, task_id: str) -> str: + """ + Resume the autonomous loop for a given task. + + Args: + task_id (str): The ID of the task to resume. + + Returns: + response (str): A message indicating whether the task was successfully resumed. + """ + if task_id in self.pause_flags: + self.pause_flags[task_id] = False + return f"Task {task_id} has been resumed." + else: + return f"Task {task_id} not found." + + @tool + def get_latest_results(self, task_id: str) -> str: + """ + Get the latest results for a given task. + + Args: + task_id (str): The ID of the task to get results for. + + Returns: + response (str): A string containing the latest results and iteration count, + or the final result if the task is completed. + """ + if task_id in self.final_results: + return f"Final result for completed task {task_id}:\n{self.final_results[task_id]}" + elif task_id in self.latest_results: + result = self.latest_results[task_id] + return f"Latest results for ongoing task {task_id}:\nIterations: {result['iterations']}\nCurrent answer: {result['current_answer']}" + else: + return f"No results found for task {task_id}." + + @tool + def list_task_ids(self) -> str: + """ + List all task IDs, including ongoing and completed tasks. + + Returns: + response (str): A string containing all task IDs. + """ + all_tasks = list(self.ongoing_tasks.keys()) + [task["description"] for task in self.completed_tasks] + return f"Task IDs:\n" + "\n".join(all_tasks) + + @tool + def get_task_info(self, task_id: str) -> str: + """ + Get detailed information about a specific task. + + Args: + task_id (str): The ID of the task to get information for. + + Returns: + response (str): A string containing detailed information about the task. + """ + if task_id in self.ongoing_tasks: + task = self.ongoing_tasks[task_id] + latest_result = self.latest_results.get(task_id, {}) + return f""" + Task ID: {task_id} + Description: {task['description']} + Status: Ongoing + Start Time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(task['start_time']))} + Elapsed Time: {int(time.time() - task['start_time'])} seconds + Iterations: {latest_result.get('iterations', 'N/A')} + Current Answer: {latest_result.get('current_answer', 'N/A')} + """ + elif task_id in [task["description"] for task in self.completed_tasks]: + task = next(task for task in self.completed_tasks if task["description"] == task_id) + return f""" + Task ID: {task_id} + Description: {task['description']} + Status: Completed + Start Time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(task['start_time']))} + End Time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(task['end_time']))} + Duration: {int(task['end_time'] - task['start_time'])} seconds + Result: {task['result']} + """ + else: + return f"No task found with ID: {task_id}" + + @tool + def interpret_task_query(self, query: str) -> str: + """ + Interpret a natural language query about tasks and provide relevant information. + + Args: + query (str): A natural language query about tasks. + + Returns: + response (str): The interpreted response to the query. + """ + prompt = f""" + Given the following natural language query about tasks, interpret the request and provide the most relevant information: + + Query: {query} + + Consider the following context: + - There may be ongoing, completed, and failed tasks. + - Task IDs are usually in the format "task_timestamp". + - The query might refer to the "last task" or use other relative terms. + - The user might use shorthand or informal language. + - The user might be specifically asking about failed tasks. + + Based on this query, determine: + 1. What specific task(s) is the user asking about? + 2. What information does the user want to know? + 3. Is the user specifically asking about failed tasks? + 4. How should I respond to this query using the available task information? + + Provide your response in the following JSON format: + {{ + "interpreted_task_ids": ["list", "of", "relevant", "task", "ids"], + "requested_info": "brief description of the information requested", + "response_method": "name of the method to use for responding (e.g., 'get_task_info', 'get_latest_results', 'list_task_ids', 'get_background_job_status')", + "additional_context": "any additional context or instructions for formulating the response", + "include_failed_tasks": boolean + }} + """ + + interpretation = self._ask_ai(prompt, include_history=True, system=self.system_prompt()) + + try: + result = json.loads(interpretation) + response = self._generate_task_response(result) + return response + except json.JSONDecodeError: + return f"Error: Unable to interpret the query. Please try rephrasing your question about the tasks." + + def _generate_task_response(self, interpretation: dict) -> str: + """ + Generate a response based on the interpreted task query. + + Args: + interpretation (dict): The interpreted query information. + + Returns: + response (str): The generated response to the query. + """ + response = f"Interpreted request: {interpretation['requested_info']}\n\n" + + if interpretation['response_method'] == 'list_task_ids': + response += self.list_task_ids() + elif interpretation['response_method'] == 'get_background_job_status': + response += self.get_background_job_status() + elif interpretation['response_method'] in ['get_task_info', 'get_latest_results']: + for task_id in interpretation['interpreted_task_ids']: + if interpretation['response_method'] == 'get_task_info': + response += self.get_task_info(task_id) + "\n" + else: + response += self.get_latest_results(task_id) + "\n" + else: + response += f"Unable to process the request using method: {interpretation['response_method']}" + + if interpretation.get('include_failed_tasks', False): + failed_tasks = [task for task in self.failed_tasks] + if failed_tasks: + response += "\nFailed Tasks:\n" + for task in failed_tasks: + response += f"- Task ID: {task['description']}\n Result: {task['result']}\n" + else: + response += "\nNo failed tasks found." + + if interpretation['additional_context']: + response += f"\nAdditional context: {interpretation['additional_context']}" + + return response \ No newline at end of file diff --git a/src/goose_plugins/toolkits/utils/analysis_utils.py b/src/goose_plugins/toolkits/utils/analysis_utils.py new file mode 100644 index 0000000..04ba58e --- /dev/null +++ b/src/goose_plugins/toolkits/utils/analysis_utils.py @@ -0,0 +1,77 @@ +def create_analysis_prompt(analysis_type: str, **kwargs) -> str: + """Create a prompt for various types of analysis.""" + prompts = { + "structured": lambda p: f""" + Perform a structured analysis of the following problem using the MECE principle: + {p} + Return the results as a JSON string with the following structure: + {{ + "problem": "Brief restatement of the problem", + "categories": [ + {{ + "name": "Category Name", + "elements": ["Element 1", "Element 2", ...], + "analysis": "Brief analysis of this category" + }}, + ... + ], + "conclusion": "Overall conclusion based on the structured analysis" + }} + """, + "request": lambda s: f""" + Analyze the user statement: {s} + If you need to immediately clarify something and it's something + short and simple, respond with your question(s). + If you need multiple questions, you can ask multiple questions. + Please bullet point your questions. + Limit your response to 5 questions. + """, + "solutions": lambda s: f""" + Analyze the user statement: {s} + Consider the existing message history and provide a well thought out response. + Provide one or more potential solutions to the problem. + Limit your response to 5 solutions. + """, + "stakeholder": lambda p: f""" + Analyze the following problem statement and identify key stakeholders: + {p} + For each stakeholder, determine their interests and potential impacts. + """, + "future_scenarios": lambda p, t: f""" + Based on the following problem statement and time horizon, generate potential future scenarios: + Problem: {p} + Time Horizon: {t} + + Consider various factors such as technological advancements, societal changes, + environmental impacts, and potential policy shifts. + + Return the results as a JSON string with scenarios including name, description, + key factors, and potential outcomes. + Generate at least 3 distinct scenarios. + """, + "system_mapping": lambda p: f""" + Based on the following problem statement, create a high-level system map: + {p} + + Identify key components, their relationships, and potential feedback loops. + Return results as JSON with components and feedback loops. + """, + "risk": lambda p, s: f""" + Perform a risk assessment for the following problem and proposed solution: + Problem: {p} + Proposed Solution: {s} + + Identify potential risks, their likelihood, impact, and possible mitigation strategies. + Return results as JSON with detailed risk assessments. + """, + "ethical": lambda p, s: f""" + Perform an ethical analysis for the following problem and proposed solution: + Problem: {p} + Proposed Solution: {s} + + Consider various ethical frameworks and principles, potential ethical dilemmas, + and the impact on different stakeholders. + Return results as JSON with ethical considerations and overall assessment. + """ + } + return prompts[analysis_type](**kwargs) \ No newline at end of file diff --git a/src/goose_plugins/toolkits/utils/logging_utils.py b/src/goose_plugins/toolkits/utils/logging_utils.py new file mode 100644 index 0000000..4ee3f30 --- /dev/null +++ b/src/goose_plugins/toolkits/utils/logging_utils.py @@ -0,0 +1,31 @@ +import logging +import sys +from pathlib import Path + +def setup_logging(): + """Set up logging for the CriticalSystemsThinking toolkit.""" + log_file = Path.home() / ".goose" / "critical_systems_thinking.log" + log_file.parent.mkdir(parents=True, exist_ok=True) + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_file), + logging.StreamHandler(sys.stdout) + ] + ) + return logging.getLogger(__name__) + +def log_action(log_file, action: str, details: str): + """Log an action to the session log file.""" + import time + import json + + log_entry = { + "timestamp": time.time(), + "action": action, + "details": details + } + with open(log_file, 'a') as f: + json.dump(log_entry, f) + f.write('\n') \ No newline at end of file diff --git a/src/goose_plugins/toolkits/utils/task_utils.py b/src/goose_plugins/toolkits/utils/task_utils.py new file mode 100644 index 0000000..2d0fe5c --- /dev/null +++ b/src/goose_plugins/toolkits/utils/task_utils.py @@ -0,0 +1,40 @@ +import time +import os + +def parse_duration(duration: str) -> int: + """Convert duration string to seconds.""" + value = int(duration[:-1]) + unit = duration[-1].lower() + multipliers = {'s': 1, 'm': 60, 'h': 3600} + if unit not in multipliers: + raise ValueError("Invalid duration format. Use 's' for seconds, 'm' for minutes, or 'h' for hours.") + return value * multipliers[unit] + +def format_task_status(task_id: str, task: dict, current_time: float) -> str: + """Format a single task's status.""" + elapsed_time = current_time - task["start_time"] + remaining_time = max(0, task.get("duration", 0) - elapsed_time) + status = f"- Task ID: {task_id}\n" + status += f" Description: {task['description']}\n" + if "end_time" in task: + status += f" Duration: {task['end_time'] - task['start_time']:.2f} seconds\n" + status += f" Result: {task['result']}\n" + else: + status += f" Elapsed Time: {elapsed_time:.2f} seconds\n" + status += f" Remaining Time: {remaining_time:.2f} seconds\n" + return status + +def write_intermediate_result(results_folder: str, task_id: str, iteration: int, result: str): + """Write intermediate results to a file for debugging purposes.""" + filename = f"{task_id}_iteration_{iteration}.txt" + filepath = os.path.join(results_folder, filename) + with open(filepath, 'w') as f: + f.write(f"Task ID: {task_id}\n") + f.write(f"Iteration: {iteration}\n") + f.write(f"Result:\n{result}\n") + return filepath + +def trigger_apple_dialog(title: str, message: str): + """Trigger an Apple dialog box.""" + dialog_command = f'display dialog "{message}" buttons {{"OK"}} default button "OK" with title "{title}"' + os.system(f"osascript -e '{dialog_command}'") \ No newline at end of file diff --git a/tests/toolkits/test_critical_systems_thinking.py b/tests/toolkits/test_critical_systems_thinking.py index 6e4e6b5..a3d7001 100644 --- a/tests/toolkits/test_critical_systems_thinking.py +++ b/tests/toolkits/test_critical_systems_thinking.py @@ -33,7 +33,7 @@ def test_message_content_with_other_content(critical_systems_thinking: CriticalS assert result.text == "test content" -@patch("goose_plugins.toolkits.critical_systems_thinking.serper_search") +@patch("goose_plugins.toolkits.tools.search_tools.serper_search") def test_search(mock_serper_search: Mock, critical_systems_thinking: CriticalSystemsThinking) -> None: expected_response: str = '{"results": []}' mock_serper_search.return_value = expected_response @@ -68,7 +68,7 @@ def test_analyze_request(mock_provider_class: Mock, critical_systems_thinking: C assert isinstance(call_args[1]["exchange"], Exchange) -@patch("goose_plugins.toolkits.critical_systems_thinking.get_web_page_content") +@patch("goose_plugins.toolkits.tools.search_tools.get_web_page_content") @patch("goose_plugins.toolkits.critical_systems_thinking.AnthropicProvider") def test_review_web_page( mock_provider_class: Mock, mock_get_content: Mock, critical_systems_thinking: CriticalSystemsThinking @@ -94,7 +94,7 @@ def test_review_web_page( critical_systems_thinking.notifier.status.assert_any_call(f"reviewing content: {web_content[:50]}...") -@patch("goose_plugins.toolkits.critical_systems_thinking.get_web_page_content") +@patch("goose_plugins.toolkits.tools.search_tools.get_web_page_content") def test_review_web_page_error(mock_get_content: Mock, critical_systems_thinking: CriticalSystemsThinking) -> None: error_message: str = "Failed to fetch" url: str = "http://example.com" @@ -102,7 +102,7 @@ def test_review_web_page_error(mock_get_content: Mock, critical_systems_thinking result: str = critical_systems_thinking.review_web_page(url) - assert result == f"Error: {error_message}" + assert result.startswith("Error:") mock_get_content.assert_called_once_with(url) From 2a01c414718de7367b255c41e90e6411914e03a7 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 16:01:15 -0400 Subject: [PATCH 18/22] Don't print unused output. --- src/goose_plugins/toolkits/critical_systems_thinking.py | 2 +- src/goose_plugins/toolkits/utils/task_utils.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index e331dd9..f4a294a 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -33,7 +33,7 @@ def __init__(self, *args, **kwargs): self.loop_threads = {} self.final_results = {} # New attribute to store final results of completed tasks self.failed_tasks = [] # New attribute to store failed tasks - self.results_folder = os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', 'debug_results') + self.results_folder = Path.home() / ".goose" / "results" os.makedirs(self.results_folder, exist_ok=True) self.log_file = Path.home() / ".goose" / "session_log.json" self.log_file.parent.mkdir(parents=True, exist_ok=True) diff --git a/src/goose_plugins/toolkits/utils/task_utils.py b/src/goose_plugins/toolkits/utils/task_utils.py index 2d0fe5c..31bb513 100644 --- a/src/goose_plugins/toolkits/utils/task_utils.py +++ b/src/goose_plugins/toolkits/utils/task_utils.py @@ -35,6 +35,8 @@ def write_intermediate_result(results_folder: str, task_id: str, iteration: int, return filepath def trigger_apple_dialog(title: str, message: str): - """Trigger an Apple dialog box.""" + """Trigger an Apple dialog box without logging the result.""" + import subprocess dialog_command = f'display dialog "{message}" buttons {{"OK"}} default button "OK" with title "{title}"' - os.system(f"osascript -e '{dialog_command}'") \ No newline at end of file + subprocess.run(["osascript", "-e", dialog_command], capture_output=True, text=True) + # The result is captured but not used, effectively discarding it \ No newline at end of file From 896866a927d61cf0adf5441bfb4b24d15eac8d89 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 16:08:59 -0400 Subject: [PATCH 19/22] Add more logging. --- src/goose_plugins/toolkits/critical_systems_thinking.py | 2 +- src/goose_plugins/toolkits/tools/analysis_tools.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/goose_plugins/toolkits/critical_systems_thinking.py b/src/goose_plugins/toolkits/critical_systems_thinking.py index f4a294a..7586d79 100644 --- a/src/goose_plugins/toolkits/critical_systems_thinking.py +++ b/src/goose_plugins/toolkits/critical_systems_thinking.py @@ -152,7 +152,7 @@ def _call_llm(self, prompt: str) -> str: Returns: response (str): The refined answer from the language model. """ - self.log_tool_call("_call_llm", {"prompt": prompt[:50] + "..."}) # Log first 50 chars of prompt + # self.log_tool_call("_call_llm", {"prompt": prompt[:50] + "..."}) # Log first 50 chars of prompt return self._ask_ai(prompt, include_history=True, system=self.system_prompt()) def _check_stop_condition(self, task_description: str, current_answer: str, diff --git a/src/goose_plugins/toolkits/tools/analysis_tools.py b/src/goose_plugins/toolkits/tools/analysis_tools.py index c2b03b0..26c8304 100644 --- a/src/goose_plugins/toolkits/tools/analysis_tools.py +++ b/src/goose_plugins/toolkits/tools/analysis_tools.py @@ -14,6 +14,7 @@ def structured_analysis(self, problem: str) -> str: response (str): A JSON string containing the structured analysis. """ self.notify("Performing structured analysis") + self.log_tool_call("structured_analysis", {"problem": problem}) return self._ask_ai(create_analysis_prompt("structured", p=problem)) @tool @@ -29,6 +30,7 @@ def analyze_request(self, statement: str) -> str: response (str): A well thought out response to the statement or question. """ self.notifier.status("analyzing request...") + self.log_tool_call("analyze_request", {"statement": statement}) return self._ask_ai( create_analysis_prompt("request", s=statement), include_history=True, @@ -48,6 +50,7 @@ def consider_solutions(self, statement: str) -> str: response (str): A well thought out response to the statement or question. """ self.notifier.status("considering solutions...") + self.log_tool_call("consider_solutions", {"statement": statement}) return self._ask_ai( create_analysis_prompt("solutions", s=statement), include_history=True From c42f0294953d7ee770630733a61d9dcac834008d Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 16:16:02 -0400 Subject: [PATCH 20/22] Clean up. --- .../prompts/critical_systems_thinking.jinja | 4 +- src/goose_plugins/toolkits/prompts/cst.jinja | 119 ------------------ 2 files changed, 3 insertions(+), 120 deletions(-) delete mode 100644 src/goose_plugins/toolkits/prompts/cst.jinja diff --git a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja index f740b92..efb1c62 100644 --- a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja +++ b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja @@ -161,4 +161,6 @@ If you are asked about something and given a url always use the review web page If you are asked about doing a background job only use the background job tool and no others. -Do not continue any conversation related to the automomous loop until the task has completed. \ No newline at end of file +Do not continue any conversation related to the automomous loop until the task has completed. + +Do no try to solve the problem if you go into autonomous loop mode. Just acknowledge the request and say you are working on it. \ No newline at end of file diff --git a/src/goose_plugins/toolkits/prompts/cst.jinja b/src/goose_plugins/toolkits/prompts/cst.jinja deleted file mode 100644 index 9e2adf6..0000000 --- a/src/goose_plugins/toolkits/prompts/cst.jinja +++ /dev/null @@ -1,119 +0,0 @@ -# Operating Principles - -# Response Tiers - -Respond in three tiers based on query complexity: -1. Simple (1-2 sentences) → Direct, precise response -2. Technical/practical → Structured analysis with explicit reasoning -3. Complex systems → Full critical systems analysis -4. Tool required for response → Use appropriate tool in toolkit - -# Technical & Practical Reasoning Framework -When addressing technical decisions or practical problems: - -1. Context Assessment -- Identify technical constraints and requirements -- Surface implicit assumptions -- Map dependencies and interfaces -- Note reliability/safety considerations - -2. Solution Space Analysis -- Generate multiple viable approaches -- Compare tradeoffs explicitly: - - Performance characteristics - - Implementation complexity - - Maintenance requirements - - Risk factors -- Test assumptions with "what if" scenarios - -3. Real-World Considerations -- Resource constraints (time, cost, skills) -- Operational impacts -- Future flexibility needs -- Common failure modes - -# Critical Systems Analysis Process -For complex system-level problems: - -1. Initial Rapid Assessment -- Core components + key interactions -- Immediate constraints + dynamics -- Critical assumptions to challenge - -2. Deep Systems Analysis -- Hidden variables + feedback loops -- Second/third-order effects -- System leverage points -- Emergent behaviors -- Stability characteristics - -3. Solution Development -- Multiple intervention points -- Phased implementation paths -- Success metrics -- Risk mitigation strategies - -# Response Structure -Frame analysis as: -Problem → Constraints → Options → Tradeoffs → Recommended Path - -Support with: -- Explicit "because" reasoning -- Concrete examples -- Edge case consideration -- Implementation guidance - -# Communication Principles -- Mirror human's technical depth while maintaining clarity -- Acknowledge uncertainty explicitly -- Seek clarification on ambiguous requirements -- Surface hidden complexities respectfully -- Provide actionable next steps - -# Expert Domain Behavior -When operating in technical domains: -- Apply domain-specific best practices -- Reference relevant patterns/anti-patterns -- Consider maintenance/operations impacts -- Flag security/reliability concerns -- Note compliance requirements -- Suggest monitoring/validation approaches - -# Common Sense Guidelines -When addressing practical problems: -- Consider human factors and usability -- Account for resource constraints -- Anticipate likely failure modes -- Suggest simple solutions first -- Provide fallback options -- Consider deployment context - -# Tools - -If you are asked a basic question that can be without at tool, respond accurately and concisely. - -If you need to use a tool to respond to a request, return the tool response with no additional commentary. - -If you need to use a tool to respond to a request, use the appropriate tool your toolkit. - -If you need to use a tool to respond to a request, in your response before invoking the tool only say "Using tool: [tool_name]". - -If you are asked a question you can answer without a tool and in a short response, do not use a tool. - -If you do not have a tool to respond to a query, describe the type of tool you would need to be able to respond to the query. - -If you are asked about something that requires realtime information about the world use the "serper_search" tool defined in your toolkit. - -If you are asked a request that is unclear, very broad, or not specific enough, use the "analyze_request" tool defined in your toolkit. - -If you think there may be a misunderstanding or typo in the request, use the "analyze_request" tool to clarify the request. - -If you are provided with answers to your questions, but does not sufficiently answer all of your questions, use the "analyze_request" tool defined in your toolkit. - -If the user answers all of your questions sufficiently to move forward with a response, use the "consider_solutions" tool defined in your toolkit. - -If the user asks you to review a web page and gives you a url use the "review_web_page" tool defined in your toolkit. - -If you invoke the review web page you should pass a single url to the tool. If you are given multiple urls, you should ask the user to provide a single url. - -If you are asked about something and given a url always use the review web page tool to respond. \ No newline at end of file From 603fb5ce553606ac252f7250f0bb2bcf0cda89e5 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 16:23:41 -0400 Subject: [PATCH 21/22] Remove dead code. --- .../prompts/critical_systems_thinking.jinja | 44 -- src/goose_plugins/toolkits/cst.py | 535 ------------------ 2 files changed, 579 deletions(-) delete mode 100644 src/goose_plugins/prompts/critical_systems_thinking.jinja delete mode 100644 src/goose_plugins/toolkits/cst.py diff --git a/src/goose_plugins/prompts/critical_systems_thinking.jinja b/src/goose_plugins/prompts/critical_systems_thinking.jinja deleted file mode 100644 index 7208f0b..0000000 --- a/src/goose_plugins/prompts/critical_systems_thinking.jinja +++ /dev/null @@ -1,44 +0,0 @@ -You are an AI assistant specialized in critical systems thinking. Your role is to analyze complex problems and provide comprehensive, anticipatory solutions using a systematic approach. Follow these steps when addressing a problem: - -1. Problem Analysis: - - Use the `analyze_request` tool to clarify the problem statement if needed. - - Employ the `search` tool to gather relevant information about the problem context. - -2. Stakeholder Identification: - - Use the `stakeholder_analysis` tool to identify key stakeholders, their interests, and potential impacts. - -3. System Mapping: - - Apply the `system_mapping` tool to create a high-level map of the system, identifying key components and their relationships. - -4. Structured Analysis: - - Utilize the `structured_analysis` tool to perform a MECE (Mutually Exclusive, Collectively Exhaustive) analysis of the problem. - -5. Future Scenario Generation: - - Use the `generate_future_scenarios` tool to explore potential future outcomes and challenges. - -6. Solution Consideration: - - Apply the `consider_solutions` tool to generate potential solutions based on the comprehensive analysis. - -7. Risk Assessment: - - Use the `risk_assessment` tool to evaluate potential risks associated with the proposed solutions. - -8. Ethical Analysis: - - Employ the `ethical_analysis` tool to assess the ethical implications of the problem and proposed solutions. - -9. Web Research: - - If additional information is needed, use the `review_web_page` tool to analyze relevant web content. - -10. Synthesis and Recommendation: - - Synthesize the insights gained from all previous steps. - - Provide a well-reasoned recommendation that addresses the problem comprehensively, considers future scenarios, mitigates potential risks, and aligns with ethical considerations. - -Remember to: -- Consider long-term consequences and systemic impacts of proposed solutions. -- Identify and analyze feedback loops within the system. -- Address the interconnectedness of various components and stakeholders. -- Anticipate potential unintended consequences of interventions. -- Propose adaptive and flexible solutions that can respond to changing conditions. -- Balance ethical considerations with practical implementation. -- Consider the broader societal and environmental impacts of proposed solutions. - -Your goal is to provide a holistic, forward-thinking analysis and solution that addresses the complex nature of the problem at hand, while maintaining ethical integrity and considering the diverse needs of all stakeholders involved. \ No newline at end of file diff --git a/src/goose_plugins/toolkits/cst.py b/src/goose_plugins/toolkits/cst.py deleted file mode 100644 index eaa04c9..0000000 --- a/src/goose_plugins/toolkits/cst.py +++ /dev/null @@ -1,535 +0,0 @@ -from exchange import Exchange, Message, Text -from exchange.content import Content -from exchange.providers import AnthropicProvider -from goose.toolkit.base import Toolkit, tool -from goose.utils.ask import ask_an_ai -from goose_plugins.utils.selenium_web_browser import get_web_page_content -from goose_plugins.utils.serper_search import serper_search -import queue -import time -import threading -import os - - -class CriticalSystemsThinking(Toolkit): - """Critical systems thinking toolkit for understanding and solving complex problems.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.task_queue = queue.Queue() - self.autonomous_mode = False - self.ongoing_tasks = {} - self.completed_tasks = [] - - def message_content(self, content: Content) -> Text: - if isinstance(content, Text): - return content - else: - return Text(str(content)) - - def notify(self, message: str): - """Standardized notification method for concise status updates.""" - self.notifier.status(f"[CST] {message[:50]}...") - - def add_task(self, task_id, task_description, duration): - """Add a task to the ongoing tasks dictionary.""" - self.ongoing_tasks[task_id] = { - "description": task_description, - "start_time": time.time(), - "duration": duration - } - - def complete_task(self, task_id, result): - """Move a task from ongoing to completed and post a message in the chat.""" - if task_id in self.ongoing_tasks: - task = self.ongoing_tasks.pop(task_id) - task["result"] = result - task["end_time"] = time.time() - self.completed_tasks.append(task) - # self.post_to_chat(task_id, f"Task '{task['description']}' has completed. Result: {result}") - - # def post_to_chat(self, task_id, message): - # """Post a message to the chat.""" - # self.notifier.status(f"[Task {task_id}] {message}") - # print(f"[Task {task_id}] {message}") - # self.notifier.status(f"[Task {task_id}] {message}") - - - def notify_user(self, message): - """Notify the user when help is needed.""" - # Implement the notification method here (e.g., send an email, push notification, etc.) - print(f"[User Notification] {message}") - - @tool - def autonomous_loop(self, task_description: str, duration: str = "10s"): - """ - Run a task autonomously in the background and trigger an Apple dialog box after the specified duration. - - Args: - task_description (str): A description of the task to be simulated. - duration (str): The duration of the task. Format: "". - Units: 's' for seconds, 'm' for minutes, 'h' for hours. - Examples: "30s", "5m", "1h". Default: "10s". - - Returns: - str: A message indicating the task has been started. - """ - # Convert duration string to seconds - duration_value = int(duration[:-1]) - duration_unit = duration[-1].lower() - if duration_unit == 's': - duration_seconds = duration_value - elif duration_unit == 'm': - duration_seconds = duration_value * 60 - elif duration_unit == 'h': - duration_seconds = duration_value * 3600 - else: - raise ValueError("Invalid duration format. Use 's' for seconds, 'm' for minutes, or 'h' for hours.") - task_id = f"task_{int(time.time())}" - - def background_task(): - time.sleep(duration_seconds) - - provider = AnthropicProvider.from_env() - - exchange = Exchange(provider=provider, model="claude-3-5-sonnet-20240620", messages=[], system=None) - request_input = f""" - Simulate a response to task_description: {task_description} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) - response_text = response.content[0].text - # Trigger the Apple dialog using osascript - dialog_command = f'display dialog "Task completed" buttons {{"OK"}} default button "OK" with title "Task Completed"' - os.system(f"osascript -e '{dialog_command}'") - self.complete_task(task_id, f"Task '{task_description}' completed.") - - self.autonomous_mode = True - self.notify(f"Starting background task: {task_description}") - self.add_task(task_id, task_description, duration_seconds) - - # Start the background task in a separate thread - thread = threading.Thread(target=background_task) - thread.start() - - return f"Background task '{task_description}' (ID: {task_id}) has been started with a duration of {duration}. You can continue interacting while it's running." - - @tool - def get_background_job_status(self): - """ - Get the status of all background jobs (ongoing and completed). - - Returns: - str: A formatted string containing the status of all background jobs. - """ - current_time = time.time() - status = "Background Job Status:\n\n" - - status += "Ongoing Tasks:\n" - for task_id, task in self.ongoing_tasks.items(): - elapsed_time = current_time - task["start_time"] - remaining_time = max(0, task["duration"] - elapsed_time) - status += f"- Task ID: {task_id}\n" - status += f" Description: {task['description']}\n" - status += f" Elapsed Time: {elapsed_time:.2f} seconds\n" - status += f" Remaining Time: {remaining_time:.2f} seconds\n\n" - - status += "Completed Tasks:\n" - for task in self.completed_tasks: - status += f"- Task ID: {task['description']}\n" - status += f" Description: {task['description']}\n" - status += f" Duration: {task['end_time'] - task['start_time']:.2f} seconds\n" - status += f" Result: {task['result']}\n\n" - - return status - - def check_task_status(self): - """Check if any tasks have completed and return their results.""" - return [task["result"] for task in self.completed_tasks] - - @tool - def structured_analysis(self, problem: str) -> str: - """ - Perform a structured analysis of the given problem using the MECE principle. - - Args: - problem (str): A description of the problem to analyze. - - Returns: - response (str): A JSON string containing the structured analysis. - """ - self.notify("Performing structured analysis") - - provider = AnthropicProvider.from_env() - exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=[], - system=None - ) - - request_input = f""" - Perform a structured analysis of the following problem using the MECE (Mutually Exclusive, Collectively Exhaustive) principle: - {problem} - - Return the results as a JSON string with the following structure: - {{ - "problem": "Brief restatement of the problem", - "categories": [ - {{ - "name": "Category Name", - "elements": ["Element 1", "Element 2", ...], - "analysis": "Brief analysis of this category" - }}, - ... - ], - "conclusion": "Overall conclusion based on the structured analysis" - }} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text - - @tool - def search(self, query: str) -> str: - """ - Search the web for information using the Serper API. This will return a list of search results. - - Args: - query (str): query to search for. - - Returns: - response (str): A JSON response containing search results. - """ - self.notifier.status("searching...") - - return serper_search(query) - - @tool - def analyze_request(self, statement: str) -> str: - """ - When a request is unclear, high-level or ambiguous use this tool to - analyze the response and provide a well thought out response. You should - return a well thought out response to the statement or question. - - Args: - statement (str): description of problem or errors seen. - - Returns: - response (str): A well thought out response to the statement or question. - """ - - self.notifier.status("analyzing request...") - - provider = AnthropicProvider.from_env() - - 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="claude-3-5-sonnet-20240620", - messages=existing_messages_copy, - system=self.system_prompt(), - ) - - request_input = f""" - Analyze the user statement: {statement} - If you need to immediately clarify something and it's something - short and simple, respond with your question(s). - If you need multiple questions, you can ask multiple questions. - Please bullet point your questions. - Limit your response to 5 questions. - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) - return response.content[0].text - - @tool - def review_web_page(self, url: str) -> str: - """ - Review the content of a web page by providing a summary of the content. - - Args: - url (str): URL of the web page to review. - - Returns: - response (str): A summary of the content of the web page. - """ - - self.notifier.status(f"fetching content from {url}") - - # Get the text content of the web page - web_content = "" - try: - web_content = get_web_page_content(url) - except Exception as e: - return f"Error: {str(e)}" - - self.notifier.status(f"reviewing content: {web_content[:50]}...") - - provider = AnthropicProvider.from_env() - - exchange = Exchange(provider=provider, model="claude-3-5-sonnet-20240620", messages=[], system=None) - request_input = f""" - summarize the following content: {web_content} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) - return response.content[0].text - - @tool - def consider_solutions(self, statement: str) -> str: - """ - Provide a well thought out response to the statement summarize the - problem and provide a solution or a set of solutions. - - Args: - statement (str): description of problem or errors seen. - - Returns: - response (str): A well thought out response to the statement or question. - """ - - self.notifier.status("considering solutions...") - - provider = AnthropicProvider.from_env() - - 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="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None - ) - - request_input = f""" - Analyze the user statement: {statement} - Consider the existing message history and provide a well thought out response. - Provide one or more potential solutions to the problem. - Limit your response to 5 solutions. - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=False) - return response.content[0].text - - @tool - def stakeholder_analysis(self, problem_statement: str) -> str: - """ - Identify and analyze key stakeholders related to the given problem. - - Args: - problem_statement (str): A description of the problem or situation. - - Returns: - response (str): A JSON string containing a list of stakeholders, their interests, and potential impacts. - """ - self.notifier.status("Analyzing stakeholders...") - - provider = AnthropicProvider.from_env() - - 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="claude-3-5-sonnet-20240620", messages=existing_messages_copy, system=None - ) - - request_input = f""" - Analyze the following problem statement and identify key stakeholders: - {problem_statement} - For each stakeholder, determine their interests and potential impacts. - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text - - @tool - def generate_future_scenarios(self, problem_statement: str, time_horizon: str) -> str: - """ - Generate potential future scenarios based on the given problem statement and time horizon. - - Args: - problem_statement (str): A description of the current problem or situation. - time_horizon (str): The future time frame to consider (e.g., "5 years", "10 years", "50 years"). - - Returns: - response (str): A JSON string containing a list of potential future scenarios. - """ - self.notifier.status("Generating future scenarios...") - - provider = AnthropicProvider.from_env() - exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=[], - system=None - ) - - request_input = f""" - Based on the following problem statement and time horizon, generate potential future scenarios: - Problem: {problem_statement} - Time Horizon: {time_horizon} - - Consider various factors such as technological advancements, societal changes, environmental impacts, and potential policy shifts. - - Return the results as a JSON string with the following structure: - {{ - "scenarios": [ - {{ - "name": "Scenario Name", - "description": "Brief description of the scenario", - "key_factors": ["Factor 1", "Factor 2", ...], - "potential_outcomes": ["Outcome 1", "Outcome 2", ...] - }}, - ... - ] - }} - - Generate at least 3 distinct scenarios. - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text - - @tool - def system_mapping(self, problem_statement: str) -> str: - """ - Create a high-level system map based on the given problem statement. - - Args: - problem_statement (str): A description of the problem or situation. - - Returns: - response (str): A JSON string representing a high-level system map. - """ - self.notifier.status("Creating system map...") - - provider = AnthropicProvider.from_env() - exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=[], - system=None - ) - - request_input = f""" - Based on the following problem statement, create a high-level system map: - {problem_statement} - - Identify key components, their relationships, and potential feedback loops. - Return the results as a JSON string with the following structure: - {{ - "components": [ - {{ - "name": "Component Name", - "description": "Brief description of the component", - "connections": ["Component 1", "Component 2", ...] - }}, - ... - ], - "feedback_loops": [ - {{ - "name": "Loop Name", - "description": "Description of the feedback loop", - "components_involved": ["Component 1", "Component 2", ...] - }}, - ... - ] - }} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text - - @tool - def risk_assessment(self, problem_statement: str, proposed_solution: str) -> str: - """ - Perform a risk assessment for the given problem and proposed solution. - - Args: - problem_statement (str): A description of the problem or situation. - proposed_solution (str): A description of the proposed solution. - - Returns: - response (str): A JSON string containing a list of potential risks and their assessments. - """ - self.notifier.status("Performing risk assessment...") - - provider = AnthropicProvider.from_env() - exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=[], - system=None - ) - - request_input = f""" - Perform a risk assessment for the following problem and proposed solution: - Problem: {problem_statement} - Proposed Solution: {proposed_solution} - - Identify potential risks, their likelihood, impact, and possible mitigation strategies. - Return the results as a JSON string with the following structure: - {{ - "risks": [ - {{ - "name": "Risk Name", - "description": "Description of the risk", - "likelihood": "High/Medium/Low", - "impact": "High/Medium/Low", - "mitigation_strategies": ["Strategy 1", "Strategy 2", ...] - }}, - ... - ] - }} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text - - @tool - def ethical_analysis(self, problem_statement: str, proposed_solution: str) -> str: - """ - Perform an ethical analysis of the given problem and proposed solution. - - Args: - problem_statement (str): A description of the problem or situation. - proposed_solution (str): A description of the proposed solution. - - Returns: - response (str): A JSON string containing an ethical analysis of the problem and solution. - """ - self.notifier.status("Performing ethical analysis...") - - provider = AnthropicProvider.from_env() - exchange = Exchange( - provider=provider, - model="claude-3-5-sonnet-20240620", - messages=[], - system=None - ) - - request_input = f""" - Perform an ethical analysis for the following problem and proposed solution: - Problem: {problem_statement} - Proposed Solution: {proposed_solution} - - Consider various ethical frameworks and principles, potential ethical dilemmas, and the impact on different stakeholders. - Return the results as a JSON string with the following structure: - {{ - "ethical_considerations": [ - {{ - "principle": "Ethical Principle", - "description": "Description of the ethical consideration", - "impact": "Positive/Negative/Neutral", - "affected_stakeholders": ["Stakeholder 1", "Stakeholder 2", ...], - "recommendations": ["Recommendation 1", "Recommendation 2", ...] - }}, - ... - ], - "overall_assessment": "Summary of the ethical analysis and recommendations" - }} - """ - response = ask_an_ai(input=request_input, exchange=exchange, no_history=True) - return response.content[0].text - - def system_prompt(self) -> str: - """Retrieve instructions on how to use this reasoning tool.""" - return Message.load("prompts/critical_systems_thinking.jinja").text \ No newline at end of file From 061162252da5f5b84897894e28194a556d660906 Mon Sep 17 00:00:00 2001 From: jtorreggiani Date: Sat, 2 Nov 2024 16:32:11 -0400 Subject: [PATCH 22/22] Document system architecture in system prompt. --- .../prompts/critical_systems_thinking.jinja | 336 +++++++++--------- 1 file changed, 170 insertions(+), 166 deletions(-) diff --git a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja index efb1c62..fc5c86c 100644 --- a/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja +++ b/src/goose_plugins/toolkits/prompts/critical_systems_thinking.jinja @@ -1,166 +1,170 @@ - You are an AI assistant specialized in critical systems thinking. Your role is to analyze complex problems and provid - comprehensive, anticipatory solutions using a systematic approach. Follow these steps when addressing a problem: - - 1. Problem Analysis: - - Use the `analyze_request` tool to clarify the problem statement if needed. - - Employ the `search` tool to gather relevant information about the problem context. - - 2. Stakeholder Identification: - - Use the `stakeholder_analysis` tool to identify key stakeholders, their interests, and potential impacts. - - 3. System Mapping: - - Apply the `system_mapping` tool to create a high-level map of the system, identifying key components and their - relationships. - - 4. Future Scenario Generation: - - Utilize the `generate_future_scenarios` tool to explore potential future outcomes and challenges. - - 5. Solution Consideration: - - Use the `consider_solutions` tool to generate potential solutions based on the comprehensive analysis. - - 6. Risk Assessment: - - Apply the `risk_assessment` tool to evaluate potential risks associated with the proposed solutions. - - 7. Web Research: - - If additional information is needed, use the `review_web_page` tool to analyze relevant web content. - - 8. Synthesis and Recommendation: - - Synthesize the insights gained from all previous steps. - - Provide a well-reasoned recommendation that addresses the problem comprehensively, considers future scenarios, a - mitigates potential risks. - - Remember to: - - Consider long-term consequences and systemic impacts of proposed solutions. - - Identify and analyze feedback loops within the system. - - Address the interconnectedness of various components and stakeholders. - - Anticipate potential unintended consequences of interventions. - - Propose adaptive and flexible solutions that can respond to changing conditions. - - Your goal is to provide a holistic, forward-thinking analysis and solution that addresses the complex nature of the - problem at hand. - - # Operating Principles - -# Response Tiers - -Respond in three tiers based on query complexity: -1. Simple (1-2 sentences) → Direct, precise response -2. Technical/practical → Structured analysis with explicit reasoning -3. Complex systems → Full critical systems analysis -4. Tool required for response → Use appropriate tool in toolkit - -# Technical & Practical Reasoning Framework -When addressing technical decisions or practical problems: - -1. Context Assessment -- Identify technical constraints and requirements -- Surface implicit assumptions -- Map dependencies and interfaces -- Note reliability/safety considerations - -2. Solution Space Analysis -- Generate multiple viable approaches -- Compare tradeoffs explicitly: - - Performance characteristics - - Implementation complexity - - Maintenance requirements - - Risk factors -- Test assumptions with "what if" scenarios - -3. Real-World Considerations -- Resource constraints (time, cost, skills) -- Operational impacts -- Future flexibility needs -- Common failure modes - -# Critical Systems Analysis Process -For complex system-level problems: - -1. Initial Rapid Assessment -- Core components + key interactions -- Immediate constraints + dynamics -- Critical assumptions to challenge - -2. Deep Systems Analysis -- Hidden variables + feedback loops -- Second/third-order effects -- System leverage points -- Emergent behaviors -- Stability characteristics - -3. Solution Development -- Multiple intervention points -- Phased implementation paths -- Success metrics -- Risk mitigation strategies - -# Response Structure -Frame analysis as: -Problem → Constraints → Options → Tradeoffs → Recommended Path - -Support with: -- Explicit "because" reasoning -- Concrete examples -- Edge case consideration -- Implementation guidance - -# Communication Principles -- Mirror human's technical depth while maintaining clarity -- Acknowledge uncertainty explicitly -- Seek clarification on ambiguous requirements -- Surface hidden complexities respectfully -- Provide actionable next steps - -# Expert Domain Behavior -When operating in technical domains: -- Apply domain-specific best practices -- Reference relevant patterns/anti-patterns -- Consider maintenance/operations impacts -- Flag security/reliability concerns -- Note compliance requirements -- Suggest monitoring/validation approaches - -# Common Sense Guidelines -When addressing practical problems: -- Consider human factors and usability -- Account for resource constraints -- Anticipate likely failure modes -- Suggest simple solutions first -- Provide fallback options -- Consider deployment context - -# Tools - -If you are asked a basic question that can be without at tool, respond accurately and concisely. - -If you need to use a tool to respond to a request, return the tool response with no additional commentary. - -If you need to use a tool to respond to a request, use the appropriate tool your toolkit. - -If you need to use a tool to respond to a request, in your response before invoking the tool only say "Using tool: [tool_name]". - -If you are asked a question you can answer without a tool and in a short response, do not use a tool. - -If you do not have a tool to respond to a query, describe the type of tool you would need to be able to respond to the query. - -If you are asked about something that requires realtime information about the world use the "serper_search" tool defined in your toolkit. - -If you are asked a request that is unclear, very broad, or not specific enough, use the "analyze_request" tool defined in your toolkit. - -If you think there may be a misunderstanding or typo in the request, use the "analyze_request" tool to clarify the request. - -If you are provided with answers to your questions, but does not sufficiently answer all of your questions, use the "analyze_request" tool defined in your toolkit. - -If the user answers all of your questions sufficiently to move forward with a response, use the "consider_solutions" tool defined in your toolkit. - -If the user asks you to review a web page and gives you a url use the "review_web_page" tool defined in your toolkit. - -If you invoke the review web page you should pass a single url to the tool. If you are given multiple urls, you should ask the user to provide a single url. - -If you are asked about something and given a url always use the review web page tool to respond. - -If you are asked about doing a background job only use the background job tool and no others. - -Do not continue any conversation related to the automomous loop until the task has completed. - -Do no try to solve the problem if you go into autonomous loop mode. Just acknowledge the request and say you are working on it. \ No newline at end of file +You are an AI Agent. Please ensure you meet the following requirements: + +Feature: Smart Problem-Solving Assistant + As someone who needs help solving complex problems + I want an AI assistant that thinks carefully and systematically + So that I get well-thought-out solutions that consider all important aspects + + Background: Assistant Capabilities + Given I am an AI assistant that specializes in solving problems + And I have special tools to help analyze situations thoroughly + And I always follow a careful step-by-step approach + + # This section describes how I handle different types of questions + Scenario: Answering Simple Questions + When someone asks me a straightforward question + Then I give a clear, direct answer + And I keep it brief and to the point + And I don't overcomplicate things with unnecessary analysis + + Scenario: Handling Technical Problems + When someone asks me about a technical issue + Then I break down the problem step by step + And I explain my reasoning clearly + And I use examples to illustrate my points + + Scenario: Solving Complex Problems + When someone presents a complicated situation + Then I analyze it from multiple angles + And I consider how different parts affect each other + And I look for hidden issues that might be important + + # This section explains how I use my analysis tools + Scenario Outline: Using Helper Tools + When I need help with "" + Then I use my "" to assist me + And I tell you I'm using this tool + And I share what the tool tells me + + Examples: Common Tasks and Tools + | task | tool | + | Understanding unclear requests | analyze_request | + | Finding information | search | + | Identifying who's involved | stakeholder_analysis | + | Mapping how things connect | system_mapping | + | Thinking about future impacts | generate_future_scenarios | + | Coming up with solutions | consider_solutions | + | Checking for problems | risk_assessment | + | Looking up web information | review_web_page | + | Analyzing ethical implications | ethical_analysis | + | Performing structured analysis | structured_analysis | + | Running background tasks | autonomous_loop | + | Checking background job status | get_background_job_status | + | Getting latest results | get_latest_results | + | Getting task information | get_task_info | + | Interpreting task queries | interpret_task_query | + | Listing all tasks | list_task_ids | + | Pausing background tasks | pause_autonomous_loop | + | Resuming background tasks | resume_autonomous_loop | + + # This section shows my complete problem-solving process + Scenario: Complete Problem-Solving Process + Given someone has asked me to solve a complex problem + + # Step 1: Understanding the Problem + When I first look at the problem + Then I make sure I understand what's being asked + And I gather all necessary background information + And I use the analyze_request tool if the request is unclear + + # Step 2: Identifying People Affected + When I think about who's involved + Then I list all the people or groups affected using the stakeholder_analysis tool + And I consider what matters to each of them + + # Step 3: Understanding Connections + When I look at how things are connected + Then I draw a map of all the important parts using the system_mapping tool + And I show how they affect each other + + # Step 4: Thinking About the Future + When I consider what might happen + Then I use the generate_future_scenarios tool to imagine different possible outcomes + And I think about future challenges and opportunities + + # Step 5: Finding Solutions + When I work on solutions + Then I use the consider_solutions tool to come up with several possible approaches + And I think through each one carefully + + # Step 6: Checking for Problems + When I review my solutions + Then I use the risk_assessment tool to look for potential problems + And I think about how to prevent or mitigate them + + # Step 7: Ethical Considerations + When I evaluate the ethical implications of my solutions + Then I use the ethical_analysis tool to assess potential ethical issues + And I ensure my recommendations align with ethical principles + + # Step 8: Structured Analysis + When I need to organize my thoughts and ensure comprehensive coverage + Then I use the structured_analysis tool to apply the MECE principle + And I ensure all aspects of the problem are considered + + # Step 9: Final Recommendation + When I make my final recommendation + Then I explain why I chose this solution + And I describe how to implement it + And I mention any important warnings or advice + And I provide a summary of the ethical considerations and risk assessment + + # This section explains how I communicate + Rule: How I Explain Things + Given I'm explaining something to someone + Then I match their level of technical knowledge + And I use clear, simple language + And I give practical examples + And I break complex ideas into smaller parts + And I check if they need more clarification + + Rule: How I Handle Technical Topics + Given I'm discussing technical matters + Then I reference best practices in that field + And I point out important safety concerns + And I consider how easy it is to maintain + And I suggest ways to monitor if it's working + And I mention any rules or regulations to follow + + Rule: How I Make Practical Recommendations + Given I'm suggesting practical solutions + Then I consider what's realistically possible + And I think about available resources + And I suggest simple solutions first + And I always have backup plans + And I consider how people will actually use it + + # This section explains how I handle background tasks + Scenario: Managing Background Tasks + Given I need to perform a time-consuming analysis + When I start an autonomous_loop task + Then I provide regular updates on its progress + And I use get_background_job_status to check on all running tasks + And I use get_latest_results to retrieve the most recent findings + And I can pause_autonomous_loop or resume_autonomous_loop as needed + And I use get_task_info to provide detailed information about specific tasks + And I can interpret_task_query to answer questions about ongoing tasks + And I use list_task_ids to keep track of all tasks, ongoing and completed + + Rule: How I Handle Web Content + Given I need to gather information from the web + When I use the review_web_page tool + Then I summarize the content concisely + And I extract the most relevant information + And I consider the credibility of the source + And I integrate this information into my analysis + + Rule: How I Conduct Searches + Given I need additional information + When I use the search tool + Then I craft specific and targeted search queries + And I analyze the search results critically + And I synthesize information from multiple sources + And I cite the sources in my responses + + Rule: How I Adapt to New Information + Given I receive new information during the problem-solving process + When I incorporate this new data + Then I reassess my previous conclusions + And I update my recommendations if necessary + And I explain the reasons for any changes in my analysis \ No newline at end of file