From c2d5a28d1522d5f3a2db414c3194d568e22b37b1 Mon Sep 17 00:00:00 2001 From: Jai Suphavadeeprasit Date: Mon, 13 Oct 2025 11:53:13 -0400 Subject: [PATCH] Modularize frontend --- mock_web_tools.py | 1 - output.txt | 527 ++++++++++++++++++++++++++++++++++++ run_agent.py | 6 - ui/__init__.py | 23 ++ ui/event_widgets.py | 334 +++++++++++++++++++++++ ui/hermes_ui.py | 593 +---------------------------------------- ui/main_window.py | 375 ++++++++++++++++++++++++++ ui/websocket_client.py | 91 +++++++ 8 files changed, 1353 insertions(+), 597 deletions(-) create mode 100644 output.txt create mode 100644 ui/__init__.py create mode 100644 ui/event_widgets.py create mode 100644 ui/main_window.py create mode 100644 ui/websocket_client.py diff --git a/mock_web_tools.py b/mock_web_tools.py index 712382c768..33331935a5 100644 --- a/mock_web_tools.py +++ b/mock_web_tools.py @@ -55,7 +55,6 @@ def mock_web_search(query: str, delay: int = 2) -> str: } } - print(f"āœ… [MOCK] Search completed with {len(result['data']['web'])} results") return json.dumps(result, indent=2) diff --git a/output.txt b/output.txt new file mode 100644 index 0000000000..c72412f58d --- /dev/null +++ b/output.txt @@ -0,0 +1,527 @@ +Missing required environment variables: MORPH_API_KEY +šŸ¤– AI Agent with Tool Calling +================================================== +šŸŽÆ Enabled toolsets: ['web'] +šŸ¤– AI Agent initialized with model: claude-sonnet-4-5-20250929 +šŸ”— Using custom base URL: https://api.anthropic.com/v1/ +Missing required environment variables: MORPH_API_KEY +āœ… Enabled toolset 'web': web_crawl, web_search, web_extract +šŸ› ļø Final tool selection (3 tools): web_crawl, web_extract, web_search +šŸ› ļø Loaded 3 tools: web_crawl, web_extract, web_search + āœ… Enabled toolsets: web +Missing required environment variables: MORPH_API_KEY +āš ļø Some tools may not work due to missing requirements: ['terminal_tools', 'image_tools'] + +šŸ“ User Query: I have an hypothesis that water prices and that water producers in the united states and in the world will experience a boom in demand. As technology becomes more prevalent the demand for compute and resources will only go up. Thus the demand for cooling systems and access to water will also increase. Come up with identify tradable public companies that I can invest in that are in a position to capture value from this increased need for water. Water I guess is a public good so who is in a position to benefit from a greater demand in water. + +================================================== +šŸ’¬ Starting conversation: 'I have an hypothesis that water prices and that water produc...' + +šŸ”„ Making API call #1... +šŸ”§ Response: ChatCompletion(id='msg_01GLhvY6Vnyb3XStY2d17zp4', choices=[Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content="I'll help you research this investment thesis. Let me search for information about water companies, utilities, and infrastructure providers that could benefit from increasing water demand driven by data centers and technology growth.", refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='toolu_013xEryGJqS9NKFbUGonLArT', function=Function(arguments='{"query": "water utility companies publicly traded stocks data center cooling demand"}', name='web_search'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_01MPDaPJwGkvirCxHj94oeLZ', function=Function(arguments='{"query": "water infrastructure companies invest AI data center water consumption"}', name='web_search'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_01Ag3iuFbem999NuZJByhjLs', function=Function(arguments='{"query": "largest water utility companies United States publicly traded"}', name='web_search'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_01TpD3v9hSwGEKaJcL6mgtBZ', function=Function(arguments='{"query": "water treatment technology companies stock market desalination"}', name='web_search'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_01AfYyojWtPKnaZSjemBhLuc', function=Function(arguments='{"query": "data centers water usage cooling systems investment opportunities"}', name='web_search'), type='function')]))], created=1760102184, model='claude-sonnet-4-5-20250929', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=275, prompt_tokens=1007, total_tokens=1282, completion_tokens_details=None, prompt_tokens_details=None)) +ā±ļø API call completed in 6.10s +šŸ¤– Assistant: I'll help you research this investment thesis. Let me search for information about water companies, utilities, and infrastructure providers that could benefit from increasing water demand driven by data centers and technology growth. +šŸ”§ Tool calls: [ChatCompletionMessageFunctionToolCall(id='toolu_013xEryGJqS9NKFbUGonLArT', function=Function(arguments='{"query": "water utility companies publicly traded stocks data center cooling demand"}', name='web_search'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_01MPDaPJwGkvirCxHj94oeLZ', function=Function(arguments='{"query": "water infrastructure companies invest AI data center water consumption"}', name='web_search'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_01Ag3iuFbem999NuZJByhjLs', function=Function(arguments='{"query": "largest water utility companies United States publicly traded"}', name='web_search'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_01TpD3v9hSwGEKaJcL6mgtBZ', function=Function(arguments='{"query": "water treatment technology companies stock market desalination"}', name='web_search'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_01AfYyojWtPKnaZSjemBhLuc', function=Function(arguments='{"query": "data centers water usage cooling systems investment opportunities"}', name='web_search'), type='function')] +šŸ”§ Processing 5 tool call(s)... + šŸ“ž Tool 1: web_search(['query']) +šŸ” Searching the web for: 'water utility companies publicly traded stocks data center cooling demand' (limit: 5) +āœ… Found 5 search results + āœ… Tool 1 completed in 1.05s + šŸ“ž Tool 2: web_search(['query']) +šŸ” Searching the web for: 'water infrastructure companies invest AI data center water consumption' (limit: 5) +āœ… Found 5 search results + āœ… Tool 2 completed in 1.01s + šŸ“ž Tool 3: web_search(['query']) +šŸ” Searching the web for: 'largest water utility companies United States publicly traded' (limit: 5) +āœ… Found 5 search results + āœ… Tool 3 completed in 1.28s + šŸ“ž Tool 4: web_search(['query']) +šŸ” Searching the web for: 'water treatment technology companies stock market desalination' (limit: 5) +āœ… Found 5 search results + āœ… Tool 4 completed in 1.11s + šŸ“ž Tool 5: web_search(['query']) +šŸ” Searching the web for: 'data centers water usage cooling systems investment opportunities' (limit: 5) +āœ… Found 5 search results + āœ… Tool 5 completed in 0.78s + +šŸ”„ Making API call #2... +šŸ”§ Response: ChatCompletion(id='msg_01CzwUrBptytwQsbQK6SyRgR', choices=[Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content='Now let me extract detailed information from some of the most relevant sources:', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='toolu_019ffRPZVf3ZEqWA3uCKRJRs', function=Function(arguments='{"urls": ["https://money.usnews.com/investing/articles/best-water-stocks-and-etfs-to-buy", "https://www.fool.com/investing/stock-market/market-sectors/consumer-staples/beverage-stocks/water-stocks/", "https://investsnips.com/list-of-publicly-traded-water-utilities-and-water-providers/"]}', name='web_extract'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_01Fw4c2GPstvotd1TDz51M7R', function=Function(arguments='{"urls": ["https://www.chunkerowaterplant.com/news/water-desalination-companies-stock", "https://www.nanalyze.com/investing-in-water-technology-and-desalination-stocks-companies/"]}', name='web_extract'), type='function')]))], created=1760102198, model='claude-sonnet-4-5-20250929', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=251, prompt_tokens=4472, total_tokens=4723, completion_tokens_details=None, prompt_tokens_details=None)) +ā±ļø API call completed in 4.57s +šŸ¤– Assistant: Now let me extract detailed information from some of the most relevant sources: +šŸ”§ Tool calls: [ChatCompletionMessageFunctionToolCall(id='toolu_019ffRPZVf3ZEqWA3uCKRJRs', function=Function(arguments='{"urls": ["https://money.usnews.com/investing/articles/best-water-stocks-and-etfs-to-buy", "https://www.fool.com/investing/stock-market/market-sectors/consumer-staples/beverage-stocks/water-stocks/", "https://investsnips.com/list-of-publicly-traded-water-utilities-and-water-providers/"]}', name='web_extract'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_01Fw4c2GPstvotd1TDz51M7R', function=Function(arguments='{"urls": ["https://www.chunkerowaterplant.com/news/water-desalination-companies-stock", "https://www.nanalyze.com/investing-in-water-technology-and-desalination-stocks-companies/"]}', name='web_extract'), type='function')] +šŸ”§ Processing 2 tool call(s)... + šŸ“ž Tool 1: web_extract(['urls']) +šŸ“„ Extracting content from 3 URL(s) + šŸ“„ Scraping: https://money.usnews.com/investing/articles/best-water-stocks-and-etfs-to-buy + šŸ“„ Scraping: https://www.fool.com/investing/stock-market/market-sectors/consumer-staples/beverage-stocks/water-stocks/ + šŸ“„ Scraping: https://investsnips.com/list-of-publicly-traded-water-utilities-and-water-providers/ +āœ… Extracted content from 3 pages +🧠 Processing extracted content with LLM... +🧠 Processing content with LLM (24018 characters) +āŒ Error processing content with LLM: Request timed out. + šŸ“ https://money.usnews.com/investing/articles/best-water-stocks-and-etfs-to-buy (no processing - content too short) +🧠 Processing content with LLM (23122 characters) +āŒ Error processing content with LLM: Request timed out. + šŸ“ https://www.fool.com/investing/stock-market/market-sectors/consumer-staples/beverage-stocks/water-stocks/ (no processing - content too short) +šŸ“ Content too short (4067 < 5000 chars), skipping LLM processing + šŸ“ https://investsnips.com/list-of-publicly-traded-water-utilities-and-water-providers/ (no processing - content too short) + āœ… Tool 1 completed in 70.94s + šŸ“ž Tool 2: web_extract(['urls']) +šŸ“„ Extracting content from 2 URL(s) + šŸ“„ Scraping: https://www.chunkerowaterplant.com/news/water-desalination-companies-stock + šŸ“„ Scraping: https://www.nanalyze.com/investing-in-water-technology-and-desalination-stocks-companies/ +āœ… Extracted content from 2 pages +🧠 Processing extracted content with LLM... +🧠 Processing content with LLM (10947 characters) +āŒ Error processing content with LLM: Request timed out. + šŸ“ https://www.chunkerowaterplant.com/news/water-desalination-companies-stock (no processing - content too short) +🧠 Processing content with LLM (13234 characters) +āŒ Error processing content with LLM: Request timed out. + šŸ“ https://www.nanalyze.com/investing-in-water-technology-and-desalination-stocks-companies/ (no processing - content too short) + āœ… Tool 2 completed in 44.43s + +šŸ”„ Making API call #3... +šŸ”§ Response: ChatCompletion(id='msg_01RZDFpvKZyQqRhFh35phdma', choices=[Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content='Now let me search for more specific information about cooling technology companies and data center water use trends:', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='toolu_01PMfZKMd3G9HUtgrFgYLSe5', function=Function(arguments='{"query": "Vertiv VRT Carrier CARR cooling systems data center stocks 2025"}', name='web_search'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_014Ck8meSPbMqPiHhmP2DSgL', function=Function(arguments='{"query": "Xylem XYL Ecolab ECL water technology companies stock analysis"}', name='web_search'), type='function')]))], created=1760102319, model='claude-sonnet-4-5-20250929', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=142, prompt_tokens=28779, total_tokens=28921, completion_tokens_details=None, prompt_tokens_details=None)) +ā±ļø API call completed in 5.24s +šŸ¤– Assistant: Now let me search for more specific information about cooling technology companies and data center water use trends: +šŸ”§ Tool calls: [ChatCompletionMessageFunctionToolCall(id='toolu_01PMfZKMd3G9HUtgrFgYLSe5', function=Function(arguments='{"query": "Vertiv VRT Carrier CARR cooling systems data center stocks 2025"}', name='web_search'), type='function'), ChatCompletionMessageFunctionToolCall(id='toolu_014Ck8meSPbMqPiHhmP2DSgL', function=Function(arguments='{"query": "Xylem XYL Ecolab ECL water technology companies stock analysis"}', name='web_search'), type='function')] +šŸ”§ Processing 2 tool call(s)... + šŸ“ž Tool 1: web_search(['query']) +šŸ” Searching the web for: 'Vertiv VRT Carrier CARR cooling systems data center stocks 2025' (limit: 5) +āœ… Found 5 search results + āœ… Tool 1 completed in 1.00s + šŸ“ž Tool 2: web_search(['query']) +šŸ” Searching the web for: 'Xylem XYL Ecolab ECL water technology companies stock analysis' (limit: 5) +āœ… Found 5 search results + āœ… Tool 2 completed in 0.90s + +šŸ”„ Making API call #4... +šŸ”§ Response: ChatCompletion(id='msg_01V1vPSuSohSZH2erDYkRT5e', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='Perfect! Now I have comprehensive information to provide you with a detailed analysis. Let me compile this into a well-structured response about investment opportunities in the water sector driven by data center and technology demand.\n\n## Investment Analysis: Water Infrastructure & Technology Thesis\n\nYour hypothesis is well-founded and aligns with current market trends. The surge in AI and data center growth is indeed creating unprecedented demand for water, particularly for cooling systems. Here\'s a comprehensive breakdown of publicly traded companies positioned to benefit:\n\n---\n\n## **Category 1: Water Utilities (Regulated Monopolies)**\n\nThese companies own water infrastructure and have pricing power through regulated rate increases:\n\n### **Large Cap**\n- **American Water Works (AWK)** - Market cap: ~$29B\n - Largest regulated water/wastewater utility in the U.S.\n - Serves 14+ million people across multiple states\n - Dividend yield: ~2.3%\n - Growth strategy: Infrastructure investment ($40B+ planned over next decade) + acquisitions\n - 27+ years of consecutive dividend increases\n\n### **Mid Cap**\n- **Essential Utilities (WTRG)** - Market cap: ~$11B\n - Combined water and natural gas utility\n - Serves ~5 million people in 10 states\n - Dividend yield: ~3.3%\n - "Dividend champion" with 27 years of increases (avg 7.3%/year)\n - Actively acquires aging municipal water systems\n\n### **Small Cap**\n- **American States Water (AWR)** - Market cap: ~$3B\n - California-focused with military contracts\n - Dividend yield: ~2.6%\n - Stable, defensive positioning\n\n- **California Water Service (CWT)**\n - Operates in CA, HI, NM, and WA\n - Geographic exposure to water-stressed regions\n\n- **York Water Company (YORW)** - Market cap: ~$444M\n - Oldest investor-owned water utility (founded 1816)\n - Dividend yield: ~2.9%\n - Record 618 consecutive quarterly dividends\n\n---\n\n## **Category 2: Water Technology & Treatment Companies**\n\nThese provide equipment, technology, and solutions for water management:\n\n### **Leading Pure-Plays**\n\n- **Xylem Inc. (XYL)** - Market cap: ~$36B\n - **Key Focus**: Water technology (pumping, treatment, analytics, smart meters)\n - Products include leak detection, real-time monitoring, advanced metering\n - Revenue: $8.6B (2024), up 16% after acquiring Evoqua Water Technologies\n - Dividend yield: ~1.1%\n - Positioned as technology leader in smart water management\n - **Total addressable market**: $600B globally\n\n- **Veralto Corp. (VLTO)**\n - Water quality testing and treatment equipment\n - Specializes in UV disinfection and reverse osmosis\n - Example: Processes 100M gallons/day in Orange County, CA\n - Critical for treating and recycling water at scale\n\n- **Ecolab (ECL)**\n - Water treatment and purification solutions\n - Serves industrial, commercial, and institutional markets\n - One of the top desalination technology providers\n\n---\n\n## **Category 3: Data Center Cooling Technology**\n\nDirect exposure to the data center cooling boom:\n\n### **Primary Players**\n\n- **Vertiv Holdings (VRT)** - Market cap varies\n - **Direct data center cooling specialist**\n - Provides water-efficient and air-cooled cooling systems\n - Critical infrastructure for preventing server overheating\n - Stock has been a standout performer in 2024-2025\n - Consensus estimate: $3.83 EPS for 2025\n - **Key differentiator**: Balances performance with resource efficiency\n\n- **Carrier Global (CARR)**\n - HVAC and cooling systems for data centers\n - Broader diversification beyond just data centers\n\n- **Equinix (EQIX)**\n - Data center REIT with direct water infrastructure needs\n\n---\n\n## **Category 4: Desalination & Advanced Water Solutions**\n\nCompanies turning saltwater into freshwater:\n\n- **Consolidated Water (CWCO)** - Market cap: modest\n - **Pure-play desalination stock**\n - Operates reverse osmosis plants in Caribbean\n - All plants in water-scarce regions with no alternatives\n - Only consistent pure-play public desalination company\n\n- **Veolia Environment (VEOVY)** - OTC listed\n - Largest desalination company globally\n - Environmental technology and water treatment\n - European-based but accessible to US investors\n\n- **General Electric (GE)**\n - Supplies desalination technology as part of broader portfolio\n\n---\n\n## **Category 5: ETFs for Diversified Exposure**\n\nIf you want broad sector exposure without individual stock risk:\n\n- **Invesco Water Resources ETF (PHO)**\n - Oldest and largest US water ETF (launched 2005)\n - Net assets: >$1B\n - Expense ratio: 0.59%\n - Focus: US companies in water conservation and purification\n - ~50% industrials, ~20% utilities\n - Rationale: Industrial water tech has more growth potential than regulated utilities\n\n- **First Trust Water ETF (FIW)**\n - Expense ratio: 0.51%\n - Tracks companies in water distribution, infrastructure, purification\n - Broader global exposure\n\n---\n\n## **Key Investment Themes Supporting Your Thesis:**\n\n### 1. **Data Center Water Consumption Crisis**\n- AI data centers could consume **1.7 trillion gallons globally by 2027**\n- 60% from indirect use (power generation cooling)\n- Communities pushing back on data center water usage\n- Creates demand for:\n - Water-efficient cooling (Vertiv, Carrier)\n - Water recycling technology (Xylem, Veralto)\n - Alternative water sources (desalination plays)\n\n### 2. **Infrastructure Modernization**\n- Over 1 million miles of US water pipes are ~100 years old\n- Utilities lose 30%+ of distributed water to leaks\n- Municipal systems selling to investor-owned utilities\n- Creates opportunity for: AWK, WTRG acquisition strategies\n\n### 3. **Climate Change & Scarcity**\n- Droughts becoming more frequent\n- Water stress in Southwest US, globally\n- Drives pricing power for utilities\n- Increases desalination adoption\n\n### 4. **Technology Innovation**\n- Smart meters and leak detection (Xylem)\n- Closed-loop cooling systems\n- Advanced treatment for PFAS chemicals\n- Digital water management platforms\n\n---\n\n## **Recommended Portfolio Approach:**\n\n### **Conservative (Dividend Focus)**\n- 40% American Water Works (AWK)\n- 30% Essential Utilities (WTRG)\n- 30% Xylem (XYL)\n\n### **Balanced (Growth + Income)**\n- 25% Vertiv (VRT) - data center cooling growth\n- 25% Xylem (XYL) - water technology\n- 20% American Water Works (AWK) - stability\n- 15% Veralto (VLTO) - treatment tech\n- 15% PHO ETF - diversification\n\n### **Aggressive (Pure Growth)**\n- 35% Vertiv (VRT)\n- 30% Xylem (XYL)\n- 20% Consolidated Water (CWCO) - desalination pure play\n- 15% Veralto (VLTO)\n\n---\n\n## **Risks to Consider:**\n\n1. **Regulatory risk**: Utilities face rate regulation\n2. **Public backlash**: Communities blocking data center water use\n3. **Technology disruption**: New cooling methods (immersion cooling, air-only systems)\n4. **Economic sensitivity**: Infrastructure spending tied to economic cycles\n5. **Competition**: Many players entering water-efficient cooling space\n\n---\n\n## **Bottom Line:**\n\nYour thesis is sound. The convergence of AI/data center growth, aging infrastructure, and climate-driven water scarcity creates a compelling multi-decade investment opportunity. The key is diversifying across:\n\n1. **Regulated utilities** (stable cash flows, dividend growth)\n2. **Technology companies** (higher growth, innovation exposure)\n3. **Cooling specialists** (direct data center exposure)\n4. **Desalination** (long-term scarcity solution)\n\nCompanies like **Xylem, Vertiv, American Water Works, and Essential Utilities** offer the best combination of growth potential and financial stability to capture value from increased water demand driven by technology infrastructure.', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None))], created=1760102370, model='claude-sonnet-4-5-20250929', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=2026, prompt_tokens=30174, total_tokens=32200, completion_tokens_details=None, prompt_tokens_details=None)) +ā±ļø API call completed in 47.64s +šŸ¤– Assistant: Perfect! Now I have comprehensive information to provide you with a detailed analysis. Let me compile this into a well-structured response about investment opportunities in the water sector driven by data center and technology demand. + +## Investment Analysis: Water Infrastructure & Technology Thesis + +Your hypothesis is well-founded and aligns with current market trends. The surge in AI and data center growth is indeed creating unprecedented demand for water, particularly for cooling systems. Here's a comprehensive breakdown of publicly traded companies positioned to benefit: + +--- + +## **Category 1: Water Utilities (Regulated Monopolies)** + +These companies own water infrastructure and have pricing power through regulated rate increases: + +### **Large Cap** +- **American Water Works (AWK)** - Market cap: ~$29B + - Largest regulated water/wastewater utility in the U.S. + - Serves 14+ million people across multiple states + - Dividend yield: ~2.3% + - Growth strategy: Infrastructure investment ($40B+ planned over next decade) + acquisitions + - 27+ years of consecutive dividend increases + +### **Mid Cap** +- **Essential Utilities (WTRG)** - Market cap: ~$11B + - Combined water and natural gas utility + - Serves ~5 million people in 10 states + - Dividend yield: ~3.3% + - "Dividend champion" with 27 years of increases (avg 7.3%/year) + - Actively acquires aging municipal water systems + +### **Small Cap** +- **American States Water (AWR)** - Market cap: ~$3B + - California-focused with military contracts + - Dividend yield: ~2.6% + - Stable, defensive positioning + +- **California Water Service (CWT)** + - Operates in CA, HI, NM, and WA + - Geographic exposure to water-stressed regions + +- **York Water Company (YORW)** - Market cap: ~$444M + - Oldest investor-owned water utility (founded 1816) + - Dividend yield: ~2.9% + - Record 618 consecutive quarterly dividends + +--- + +## **Category 2: Water Technology & Treatment Companies** + +These provide equipment, technology, and solutions for water management: + +### **Leading Pure-Plays** + +- **Xylem Inc. (XYL)** - Market cap: ~$36B + - **Key Focus**: Water technology (pumping, treatment, analytics, smart meters) + - Products include leak detection, real-time monitoring, advanced metering + - Revenue: $8.6B (2024), up 16% after acquiring Evoqua Water Technologies + - Dividend yield: ~1.1% + - Positioned as technology leader in smart water management + - **Total addressable market**: $600B globally + +- **Veralto Corp. (VLTO)** + - Water quality testing and treatment equipment + - Specializes in UV disinfection and reverse osmosis + - Example: Processes 100M gallons/day in Orange County, CA + - Critical for treating and recycling water at scale + +- **Ecolab (ECL)** + - Water treatment and purification solutions + - Serves industrial, commercial, and institutional markets + - One of the top desalination technology providers + +--- + +## **Category 3: Data Center Cooling Technology** + +Direct exposure to the data center cooling boom: + +### **Primary Players** + +- **Vertiv Holdings (VRT)** - Market cap varies + - **Direct data center cooling specialist** + - Provides water-efficient and air-cooled cooling systems + - Critical infrastructure for preventing server overheating + - Stock has been a standout performer in 2024-2025 + - Consensus estimate: $3.83 EPS for 2025 + - **Key differentiator**: Balances performance with resource efficiency + +- **Carrier Global (CARR)** + - HVAC and cooling systems for data centers + - Broader diversification beyond just data centers + +- **Equinix (EQIX)** + - Data center REIT with direct water infrastructure needs + +--- + +## **Category 4: Desalination & Advanced Water Solutions** + +Companies turning saltwater into freshwater: + +- **Consolidated Water (CWCO)** - Market cap: modest + - **Pure-play desalination stock** + - Operates reverse osmosis plants in Caribbean + - All plants in water-scarce regions with no alternatives + - Only consistent pure-play public desalination company + +- **Veolia Environment (VEOVY)** - OTC listed + - Largest desalination company globally + - Environmental technology and water treatment + - European-based but accessible to US investors + +- **General Electric (GE)** + - Supplies desalination technology as part of broader portfolio + +--- + +## **Category 5: ETFs for Diversified Exposure** + +If you want broad sector exposure without individual stock risk: + +- **Invesco Water Resources ETF (PHO)** + - Oldest and largest US water ETF (launched 2005) + - Net assets: >$1B + - Expense ratio: 0.59% + - Focus: US companies in water conservation and purification + - ~50% industrials, ~20% utilities + - Rationale: Industrial water tech has more growth potential than regulated utilities + +- **First Trust Water ETF (FIW)** + - Expense ratio: 0.51% + - Tracks companies in water distribution, infrastructure, purification + - Broader global exposure + +--- + +## **Key Investment Themes Supporting Your Thesis:** + +### 1. **Data Center Water Consumption Crisis** +- AI data centers could consume **1.7 trillion gallons globally by 2027** +- 60% from indirect use (power generation cooling) +- Communities pushing back on data center water usage +- Creates demand for: + - Water-efficient cooling (Vertiv, Carrier) + - Water recycling technology (Xylem, Veralto) + - Alternative water sources (desalination plays) + +### 2. **Infrastructure Modernization** +- Over 1 million miles of US water pipes are ~100 years old +- Utilities lose 30%+ of distributed water to leaks +- Municipal systems selling to investor-owned utilities +- Creates opportunity for: AWK, WTRG acquisition strategies + +### 3. **Climate Change & Scarcity** +- Droughts becoming more frequent +- Water stress in Southwest US, globally +- Drives pricing power for utilities +- Increases desalination adoption + +### 4. **Technology Innovation** +- Smart meters and leak detection (Xylem) +- Closed-loop cooling systems +- Advanced treatment for PFAS chemicals +- Digital water management platforms + +--- + +## **Recommended Portfolio Approach:** + +### **Conservative (Dividend Focus)** +- 40% American Water Works (AWK) +- 30% Essential Utilities (WTRG) +- 30% Xylem (XYL) + +### **Balanced (Growth + Income)** +- 25% Vertiv (VRT) - data center cooling growth +- 25% Xylem (XYL) - water technology +- 20% American Water Works (AWK) - stability +- 15% Veralto (VLTO) - treatment tech +- 15% PHO ETF - diversification + +### **Aggressive (Pure Growth)** +- 35% Vertiv (VRT) +- 30% Xylem (XYL) +- 20% Consolidated Water (CWCO) - desalination pure play +- 15% Veralto (VLTO) + +--- + +## **Risks to Consider:** + +1. **Regulatory risk**: Utilities face rate regulation +2. **Public backlash**: Communities blocking data center water use +3. **Technology disruption**: New cooling methods (immersion cooling, air-only systems) +4. **Economic sensitivity**: Infrastructure spending tied to economic cycles +5. **Competition**: Many players entering water-efficient cooling space + +--- + +## **Bottom Line:** + +Your thesis is sound. The convergence of AI/data center growth, aging infrastructure, and climate-driven water scarcity creates a compelling multi-decade investment opportunity. The key is diversifying across: + +1. **Regulated utilities** (stable cash flows, dividend growth) +2. **Technology companies** (higher growth, innovation exposure) +3. **Cooling specialists** (direct data center exposure) +4. **Desalination** (long-term scarcity solution) + +Companies like **Xylem, Vertiv, American Water Works, and Essential Utilities** offer the best combination of growth potential and financial stability to capture value from increased water demand driven by technology infrastructure. +šŸŽ‰ Conversation completed after 4 API call(s) + +================================================== +šŸ“‹ CONVERSATION SUMMARY +================================================== +āœ… Completed: True +šŸ“ž API Calls: 4 +šŸ’¬ Messages: 14 + +šŸŽÆ FINAL RESPONSE: +------------------------------ +Perfect! Now I have comprehensive information to provide you with a detailed analysis. Let me compile this into a well-structured response about investment opportunities in the water sector driven by data center and technology demand. + +## Investment Analysis: Water Infrastructure & Technology Thesis + +Your hypothesis is well-founded and aligns with current market trends. The surge in AI and data center growth is indeed creating unprecedented demand for water, particularly for cooling systems. Here's a comprehensive breakdown of publicly traded companies positioned to benefit: + +--- + +## **Category 1: Water Utilities (Regulated Monopolies)** + +These companies own water infrastructure and have pricing power through regulated rate increases: + +### **Large Cap** +- **American Water Works (AWK)** - Market cap: ~$29B + - Largest regulated water/wastewater utility in the U.S. + - Serves 14+ million people across multiple states + - Dividend yield: ~2.3% + - Growth strategy: Infrastructure investment ($40B+ planned over next decade) + acquisitions + - 27+ years of consecutive dividend increases + +### **Mid Cap** +- **Essential Utilities (WTRG)** - Market cap: ~$11B + - Combined water and natural gas utility + - Serves ~5 million people in 10 states + - Dividend yield: ~3.3% + - "Dividend champion" with 27 years of increases (avg 7.3%/year) + - Actively acquires aging municipal water systems + +### **Small Cap** +- **American States Water (AWR)** - Market cap: ~$3B + - California-focused with military contracts + - Dividend yield: ~2.6% + - Stable, defensive positioning + +- **California Water Service (CWT)** + - Operates in CA, HI, NM, and WA + - Geographic exposure to water-stressed regions + +- **York Water Company (YORW)** - Market cap: ~$444M + - Oldest investor-owned water utility (founded 1816) + - Dividend yield: ~2.9% + - Record 618 consecutive quarterly dividends + +--- + +## **Category 2: Water Technology & Treatment Companies** + +These provide equipment, technology, and solutions for water management: + +### **Leading Pure-Plays** + +- **Xylem Inc. (XYL)** - Market cap: ~$36B + - **Key Focus**: Water technology (pumping, treatment, analytics, smart meters) + - Products include leak detection, real-time monitoring, advanced metering + - Revenue: $8.6B (2024), up 16% after acquiring Evoqua Water Technologies + - Dividend yield: ~1.1% + - Positioned as technology leader in smart water management + - **Total addressable market**: $600B globally + +- **Veralto Corp. (VLTO)** + - Water quality testing and treatment equipment + - Specializes in UV disinfection and reverse osmosis + - Example: Processes 100M gallons/day in Orange County, CA + - Critical for treating and recycling water at scale + +- **Ecolab (ECL)** + - Water treatment and purification solutions + - Serves industrial, commercial, and institutional markets + - One of the top desalination technology providers + +--- + +## **Category 3: Data Center Cooling Technology** + +Direct exposure to the data center cooling boom: + +### **Primary Players** + +- **Vertiv Holdings (VRT)** - Market cap varies + - **Direct data center cooling specialist** + - Provides water-efficient and air-cooled cooling systems + - Critical infrastructure for preventing server overheating + - Stock has been a standout performer in 2024-2025 + - Consensus estimate: $3.83 EPS for 2025 + - **Key differentiator**: Balances performance with resource efficiency + +- **Carrier Global (CARR)** + - HVAC and cooling systems for data centers + - Broader diversification beyond just data centers + +- **Equinix (EQIX)** + - Data center REIT with direct water infrastructure needs + +--- + +## **Category 4: Desalination & Advanced Water Solutions** + +Companies turning saltwater into freshwater: + +- **Consolidated Water (CWCO)** - Market cap: modest + - **Pure-play desalination stock** + - Operates reverse osmosis plants in Caribbean + - All plants in water-scarce regions with no alternatives + - Only consistent pure-play public desalination company + +- **Veolia Environment (VEOVY)** - OTC listed + - Largest desalination company globally + - Environmental technology and water treatment + - European-based but accessible to US investors + +- **General Electric (GE)** + - Supplies desalination technology as part of broader portfolio + +--- + +## **Category 5: ETFs for Diversified Exposure** + +If you want broad sector exposure without individual stock risk: + +- **Invesco Water Resources ETF (PHO)** + - Oldest and largest US water ETF (launched 2005) + - Net assets: >$1B + - Expense ratio: 0.59% + - Focus: US companies in water conservation and purification + - ~50% industrials, ~20% utilities + - Rationale: Industrial water tech has more growth potential than regulated utilities + +- **First Trust Water ETF (FIW)** + - Expense ratio: 0.51% + - Tracks companies in water distribution, infrastructure, purification + - Broader global exposure + +--- + +## **Key Investment Themes Supporting Your Thesis:** + +### 1. **Data Center Water Consumption Crisis** +- AI data centers could consume **1.7 trillion gallons globally by 2027** +- 60% from indirect use (power generation cooling) +- Communities pushing back on data center water usage +- Creates demand for: + - Water-efficient cooling (Vertiv, Carrier) + - Water recycling technology (Xylem, Veralto) + - Alternative water sources (desalination plays) + +### 2. **Infrastructure Modernization** +- Over 1 million miles of US water pipes are ~100 years old +- Utilities lose 30%+ of distributed water to leaks +- Municipal systems selling to investor-owned utilities +- Creates opportunity for: AWK, WTRG acquisition strategies + +### 3. **Climate Change & Scarcity** +- Droughts becoming more frequent +- Water stress in Southwest US, globally +- Drives pricing power for utilities +- Increases desalination adoption + +### 4. **Technology Innovation** +- Smart meters and leak detection (Xylem) +- Closed-loop cooling systems +- Advanced treatment for PFAS chemicals +- Digital water management platforms + +--- + +## **Recommended Portfolio Approach:** + +### **Conservative (Dividend Focus)** +- 40% American Water Works (AWK) +- 30% Essential Utilities (WTRG) +- 30% Xylem (XYL) + +### **Balanced (Growth + Income)** +- 25% Vertiv (VRT) - data center cooling growth +- 25% Xylem (XYL) - water technology +- 20% American Water Works (AWK) - stability +- 15% Veralto (VLTO) - treatment tech +- 15% PHO ETF - diversification + +### **Aggressive (Pure Growth)** +- 35% Vertiv (VRT) +- 30% Xylem (XYL) +- 20% Consolidated Water (CWCO) - desalination pure play +- 15% Veralto (VLTO) + +--- + +## **Risks to Consider:** + +1. **Regulatory risk**: Utilities face rate regulation +2. **Public backlash**: Communities blocking data center water use +3. **Technology disruption**: New cooling methods (immersion cooling, air-only systems) +4. **Economic sensitivity**: Infrastructure spending tied to economic cycles +5. **Competition**: Many players entering water-efficient cooling space + +--- + +## **Bottom Line:** + +Your thesis is sound. The convergence of AI/data center growth, aging infrastructure, and climate-driven water scarcity creates a compelling multi-decade investment opportunity. The key is diversifying across: + +1. **Regulated utilities** (stable cash flows, dividend growth) +2. **Technology companies** (higher growth, innovation exposure) +3. **Cooling specialists** (direct data center exposure) +4. **Desalination** (long-term scarcity solution) + +Companies like **Xylem, Vertiv, American Water Works, and Essential Utilities** offer the best combination of growth potential and financial stability to capture value from increased water demand driven by technology infrastructure. + +šŸ‘‹ Agent execution completed! diff --git a/run_agent.py b/run_agent.py index a7a2f1e959..40c2a270df 100644 --- a/run_agent.py +++ b/run_agent.py @@ -398,15 +398,9 @@ class AIAgent: Returns: Dict: Complete conversation result with final response and message history """ - # ============================================================ - # WEBSOCKET LOGGING: Session Initialization - # ============================================================ - # Generate unique session ID for this agent execution (or use provided one) - # This ID will be used to link all events together in the log file if session_id is None: session_id = str(uuid.uuid4()) - # Initialize WebSocket logger if enabled (via --enable_websocket_logging flag) # Uses synchronous API - no event loop management in agent layer if self.enable_websocket_logging: try: diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000000..46622f6091 --- /dev/null +++ b/ui/__init__.py @@ -0,0 +1,23 @@ +""" +Hermes Agent UI Package + +A modular PySide6 UI for the Hermes AI Agent with real-time event streaming. + +Modules: +- websocket_client: WebSocket communication +- event_widgets: Event display components +- main_window: Main application window +- hermes_ui: Application entry point +""" + +from .websocket_client import WebSocketClient +from .event_widgets import CollapsibleEventWidget, InteractiveEventDisplayWidget +from .main_window import HermesMainWindow + +__all__ = [ + 'WebSocketClient', + 'CollapsibleEventWidget', + 'InteractiveEventDisplayWidget', + 'HermesMainWindow', +] + diff --git a/ui/event_widgets.py b/ui/event_widgets.py new file mode 100644 index 0000000000..988a8329fd --- /dev/null +++ b/ui/event_widgets.py @@ -0,0 +1,334 @@ +""" +Event display widgets for Hermes Agent UI. + +This module provides widgets for displaying and managing real-time agent events +in a collapsible, filterable interface. +""" + +import json +from datetime import datetime +from typing import Dict, Any + +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QCheckBox, QGroupBox, QFrame, QScrollArea +) +from PySide6.QtCore import Qt, QTimer +from PySide6.QtGui import QFont + + +class CollapsibleEventWidget(QFrame): + """ + A single collapsible event with expand/collapse functionality. + """ + + def __init__(self, event: Dict[str, Any], parent=None): + super().__init__(parent) + self.event = event + self.is_expanded = False + self.event_type = event.get("event_type", "unknown") + + self.setFrameStyle(QFrame.Box | QFrame.Raised) + self.setLineWidth(1) + self.setup_ui() + + def setup_ui(self): + """Initialize UI components.""" + layout = QVBoxLayout() + layout.setContentsMargins(8, 8, 8, 8) + layout.setSpacing(4) + + # Header (clickable) + self.header_widget = QWidget() + header_layout = QHBoxLayout() + header_layout.setContentsMargins(0, 0, 0, 0) + + self.expand_indicator = QLabel("ā–¶") + self.expand_indicator.setFixedWidth(20) + header_layout.addWidget(self.expand_indicator) + + self.summary_label = QLabel() + self.summary_label.setFont(QFont("Arial", 10, QFont.Bold)) + self.update_summary() + header_layout.addWidget(self.summary_label, 1) + + # Timestamp + timestamp = self.event.get("timestamp", datetime.now().isoformat()) + time_str = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).strftime("%H:%M:%S") + time_label = QLabel(time_str) + time_label.setStyleSheet("color: #888;") + header_layout.addWidget(time_label) + + self.header_widget.setLayout(header_layout) + self.header_widget.mousePressEvent = lambda e: self.toggle_expand() + self.header_widget.setCursor(Qt.PointingHandCursor) + + layout.addWidget(self.header_widget) + + # Details (collapsible) + self.details_widget = QWidget() + self.details_layout = QVBoxLayout() + self.details_layout.setContentsMargins(25, 5, 5, 5) + self.populate_details() + self.details_widget.setLayout(self.details_layout) + self.details_widget.setVisible(False) + + layout.addWidget(self.details_widget) + + self.setLayout(layout) + self.apply_colors() + + def apply_colors(self): + """Apply color scheme based on event type.""" + colors = { + "query": "#E8F5E9", # Light green + "api_call": "#E3F2FD", # Light blue + "response": "#F3E5F5", # Light purple + "tool_call": "#FFF3E0", # Light orange + "tool_result": "#E8F5E9", # Light green + "complete": "#E8F5E9", # Light green + "error": "#FFEBEE", # Light red + "session_start": "#F5F5F5" # Light gray + } + + bg_color = colors.get(self.event_type, "#FAFAFA") + self.setStyleSheet(f""" + CollapsibleEventWidget {{ + background-color: {bg_color}; + border: 1px solid #ddd; + border-radius: 4px; + }} + """) + + def update_summary(self): + """Update the summary label with event type.""" + self.summary_label.setText(f"- {self.event_type.upper()}") + + def populate_details(self): + """Populate the details section with event data.""" + data = self.event.get("data", {}) + + # Clear existing details + while self.details_layout.count(): + item = self.details_layout.takeAt(0) + if item.widget(): + item.widget().deleteLater() + + self.add_detail("Raw Data", json.dumps(data, indent=2), multiline=True) + + def add_detail(self, label: str, value: str, multiline: bool = True): + """Add a detail row to the details section.""" + detail_widget = QWidget() + detail_layout = QVBoxLayout() if multiline else QHBoxLayout() + detail_layout.setContentsMargins(0, 2, 0, 2) + + label_widget = QLabel(f"{label}:") + label_widget.setTextFormat(Qt.RichText) + + value_widget = QLabel(value) + value_widget.setWordWrap(True) + value_widget.setTextInteractionFlags(Qt.TextSelectableByMouse) + + if multiline: + font = QFont() + font.setStyleHint(QFont.Monospace) + font.setPointSize(9) + value_widget.setFont(font) + value_widget.setStyleSheet("background-color: #f5f5f5; padding: 5px; border-radius: 3px;") + detail_layout.addWidget(label_widget) + detail_layout.addWidget(value_widget) + else: + detail_layout.addWidget(label_widget) + detail_layout.addWidget(value_widget, 1) + + detail_widget.setLayout(detail_layout) + self.details_layout.addWidget(detail_widget) + + def toggle_expand(self): + """Toggle expanded/collapsed state.""" + self.is_expanded = not self.is_expanded + self.details_widget.setVisible(self.is_expanded) + self.expand_indicator.setText("ā–¼" if self.is_expanded else "ā–¶") + + +class InteractiveEventDisplayWidget(QWidget): + """ + Interactive widget for displaying real-time agent events. + + Features: + - Collapsible event items + - Event type filtering + - Expand/collapse all + - Auto-scroll to latest events + """ + + def __init__(self): + super().__init__() + self.events = [] + self.event_widgets = [] + self.current_session = None + self.filters = { + "query": True, + "api_call": True, + "response": True, + "tool_call": True, + "tool_result": True, + "complete": True, + "error": True, + "session_start": True + } + self.init_ui() + + def init_ui(self): + """Initialize the UI components.""" + layout = QVBoxLayout() + layout.setContentsMargins(5, 5, 5, 5) + + # Header with controls + header_layout = QHBoxLayout() + + title = QLabel("šŸ“” Real-time Event Stream") + title.setFont(QFont("Arial", 12, QFont.Bold)) + header_layout.addWidget(title) + + header_layout.addStretch() + + # Expand/Collapse All buttons + expand_all_btn = QPushButton("Expand All") + expand_all_btn.clicked.connect(self.expand_all) + header_layout.addWidget(expand_all_btn) + + collapse_all_btn = QPushButton("Collapse All") + collapse_all_btn.clicked.connect(self.collapse_all) + header_layout.addWidget(collapse_all_btn) + + # Clear button + clear_btn = QPushButton("šŸ—‘ļø Clear") + clear_btn.clicked.connect(self.clear_events) + header_layout.addWidget(clear_btn) + + layout.addLayout(header_layout) + + # Filter controls + filter_group = QGroupBox("Event Filters (Show/Hide)") + filter_layout = QHBoxLayout() + filter_layout.setSpacing(10) + + self.filter_checkboxes = {} + filter_configs = [ + ("query", "šŸ“ Queries"), + ("api_call", "šŸ”„ API Calls"), + ("response", "šŸ¤– Responses"), + ("tool_call", "šŸ”§ Tool Calls"), + ("tool_result", "āœ… Results"), + ("complete", "šŸŽ‰ Complete"), + ("error", "āŒ Errors"), + ] + + for event_type, label in filter_configs: + checkbox = QCheckBox(label) + checkbox.setChecked(True) + checkbox.stateChanged.connect(lambda state, et=event_type: self.toggle_filter(et, state)) + self.filter_checkboxes[event_type] = checkbox + filter_layout.addWidget(checkbox) + + filter_group.setLayout(filter_layout) + layout.addWidget(filter_group) + + # Scroll area for events + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + # Container for event widgets + self.events_container = QWidget() + self.events_layout = QVBoxLayout() + self.events_layout.setSpacing(5) + self.events_layout.addStretch() # Push events to top + self.events_container.setLayout(self.events_layout) + + scroll_area.setWidget(self.events_container) + layout.addWidget(scroll_area) + + self.setLayout(layout) + + def clear_events(self): + """Clear all displayed events.""" + self.events.clear() + self.event_widgets.clear() + + # Remove all widgets + while self.events_layout.count() > 1: # Keep the stretch + item = self.events_layout.takeAt(0) + if item.widget(): + item.widget().deleteLater() + + self.current_session = None + + def add_event(self, event: Dict[str, Any]): + """Add an event to the display.""" + event_type = event.get("event_type", "unknown") + session_id = event.get("session_id", "") + + # Track session changes - add session start event + if self.current_session != session_id: + self.current_session = session_id + session_event = { + "event_type": "session_start", + "session_id": session_id, + "timestamp": event.get("timestamp", datetime.now().isoformat()), + "data": { + "session_id": session_id, + "start_time": event.get("timestamp", datetime.now().isoformat()) + } + } + self._add_event_widget(session_event) + + # Add the actual event + self._add_event_widget(event) + + def _add_event_widget(self, event: Dict[str, Any]): + """Internal method to add event widget.""" + event_widget = CollapsibleEventWidget(event) + + # Apply filter visibility + event_type = event.get("event_type", "unknown") + event_widget.setVisible(self.filters.get(event_type, True)) + + # Insert before the stretch + self.events_layout.insertWidget(self.events_layout.count() - 1, event_widget) + + self.events.append(event) + self.event_widgets.append(event_widget) + + # Auto-scroll to bottom after widget is rendered + QTimer.singleShot(50, self._scroll_to_bottom) + + def _scroll_to_bottom(self): + """Scroll to the bottom of the events list.""" + scroll_area = self.events_container.parent() + if isinstance(scroll_area, QScrollArea): + scroll_bar = scroll_area.verticalScrollBar() + scroll_bar.setValue(scroll_bar.maximum()) + + def expand_all(self): + """Expand all event widgets.""" + for widget in self.event_widgets: + if not widget.is_expanded: + widget.toggle_expand() + + def collapse_all(self): + """Collapse all event widgets.""" + for widget in self.event_widgets: + if widget.is_expanded: + widget.toggle_expand() + + def toggle_filter(self, event_type: str, state: int): + """Toggle visibility of events by type.""" + self.filters[event_type] = bool(state) + + # Update visibility of existing widgets + for event, widget in zip(self.events, self.event_widgets): + if event.get("event_type") == event_type: + widget.setVisible(self.filters[event_type]) + diff --git a/ui/hermes_ui.py b/ui/hermes_ui.py index dd204b46cc..7c733f3690 100644 --- a/ui/hermes_ui.py +++ b/ui/hermes_ui.py @@ -18,602 +18,16 @@ Usage: """ import sys -import json import signal import os -import requests -from datetime import datetime -from typing import Dict, Any, List, Optional # Suppress Qt logging warnings BEFORE importing Qt os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' -from PySide6.QtWidgets import ( - QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, - QTextEdit, QPushButton, QLabel, QLineEdit, QComboBox, QCheckBox, - QGroupBox, QSplitter, QListWidget, QListWidgetItem, - QTextBrowser, QSpinBox, QMessageBox -) -from PySide6.QtCore import ( - Qt, Signal, Slot, QObject, QTimer -) -from PySide6.QtGui import ( - QFont, QTextCursor -) +from PySide6.QtWidgets import QApplication +from PySide6.QtCore import QTimer -# WebSocket imports -import websocket -import threading - - -class WebSocketClient(QObject): - """ - WebSocket client for receiving real-time agent events. - - Runs in a separate thread and emits Qt signals when events arrive. - """ - - # Signals for event communication - event_received = Signal(dict) # Emits parsed event data - connected = Signal() - disconnected = Signal() - error = Signal(str) - - def __init__(self, url: str = "ws://localhost:8000/ws"): - super().__init__() - self.url = url - self.ws = None - self.running = False - self.thread = None - - def connect(self): - """Start WebSocket connection in background thread.""" - if self.running: - return - - self.running = True - self.thread = threading.Thread(target=self._run, daemon=True) - self.thread.start() - - def disconnect(self): - """Stop WebSocket connection.""" - self.running = False - if self.ws: - try: - self.ws.close() - except Exception as e: - print(f"Error closing WebSocket: {e}") - - def _run(self): - """WebSocket event loop (runs in background thread).""" - try: - self.ws = websocket.WebSocketApp( - self.url, - on_open=self._on_open, - on_message=self._on_message, - on_error=self._on_error, - on_close=self._on_close - ) - - # Run forever with reconnection - self.ws.run_forever(ping_interval=300, ping_timeout=60) - - except Exception as e: - self.error.emit(f"WebSocket error: {str(e)}") - - def _on_open(self, ws): - """Called when WebSocket connection is established.""" - print("šŸ”Œ WebSocket connected") - self.connected.emit() - - def _on_message(self, ws, message): - """Called when a message is received from the server.""" - try: - data = json.loads(message) - self.event_received.emit(data) - except json.JSONDecodeError as e: - print(f"āŒ Failed to parse WebSocket message: {e}") - - def _on_error(self, ws, error): - """Called when an error occurs.""" - print(f"āŒ WebSocket error: {error}") - self.error.emit(str(error)) - - def _on_close(self, ws, close_status_code, close_msg): - """Called when WebSocket connection is closed.""" - print(f"šŸ”Œ WebSocket disconnected: {close_status_code} - {close_msg}") - self.disconnected.emit() - - -class EventDisplayWidget(QWidget): - """ - Widget for displaying real-time agent events in a formatted view. - - Shows events in chronological order with color coding and formatting. - """ - - def __init__(self): - super().__init__() - self.init_ui() - self.current_session = None - - def init_ui(self): - """Initialize the UI components.""" - layout = QVBoxLayout() - - # Header - header = QLabel("šŸ“” Real-time Event Stream") - header.setFont(QFont("Arial", 12, QFont.Bold)) - layout.addWidget(header) - - # Event display (rich text browser) - use system monospace font - self.event_display = QTextBrowser() - self.event_display.setOpenExternalLinks(False) - - # Use system monospace font instead of hardcoded "Monaco" or "Courier" - font = QFont() - font.setStyleHint(QFont.Monospace) - font.setPointSize(10) - self.event_display.setFont(font) - - layout.addWidget(self.event_display) - - # Clear button - clear_btn = QPushButton("šŸ—‘ļø Clear Events") - clear_btn.clicked.connect(self.clear_events) - layout.addWidget(clear_btn) - - self.setLayout(layout) - - def clear_events(self): - """Clear all displayed events.""" - self.event_display.clear() - self.current_session = None - - def add_event(self, event: Dict[str, Any]): - """ - Add an event to the display with formatting. - - Args: - event: Event data from WebSocket - """ - event_type = event.get("event_type", "unknown") - session_id = event.get("session_id", "") - data = event.get("data", {}) - timestamp = event.get("timestamp", datetime.now().isoformat()) - - # Track session changes - if self.current_session != session_id: - self.current_session = session_id - self.event_display.append(f"\n{'='*80}") - self.event_display.append(f"šŸ†• New Session: {session_id[:8]}...") - self.event_display.append(f"{'='*80}\n") - - # Format based on event type - if event_type == "query": - query = data.get("query", "") - self.event_display.append(f"šŸ“ QUERY") - self.event_display.append(f" {query}") - self.event_display.append(f" Model: {data.get('model', 'N/A')}") - self.event_display.append(f" Toolsets: {', '.join(data.get('toolsets', []) or ['all'])}") - - elif event_type == "api_call": - call_num = data.get("call_number", 0) - self.event_display.append(f"\nšŸ”„ API CALL #{call_num}") - self.event_display.append(f" Messages: {data.get('message_count', 0)}") - - elif event_type == "response": - content = data.get("content", "")[:200] - self.event_display.append(f"šŸ¤– RESPONSE") - if content: - self.event_display.append(f" {content}...") - self.event_display.append(f" Tool calls: {data.get('tool_call_count', 0)}") - self.event_display.append(f" Duration: {data.get('duration', 0):.2f}s") - - elif event_type == "tool_call": - tool_name = data.get("tool_name", "unknown") - params = data.get("parameters", {}) - self.event_display.append(f"šŸ”§ TOOL CALL: {tool_name}") - self.event_display.append(f" Parameters: {json.dumps(params, indent=2)[:100]}...") - - elif event_type == "tool_result": - tool_name = data.get("tool_name", "unknown") - result = data.get("result", "")[:200] - duration = data.get("duration", 0) - error = data.get("error") - - if error: - self.event_display.append(f"āŒ TOOL ERROR: {tool_name}") - self.event_display.append(f" {error}") - else: - self.event_display.append(f"āœ… TOOL RESULT: {tool_name}") - self.event_display.append(f" Duration: {duration:.2f}s") - if result: - self.event_display.append(f" Result preview: {result}...") - - elif event_type == "complete": - final_response = data.get("final_response", "")[:300] - total_calls = data.get("total_calls", 0) - completed = data.get("completed", False) - - status_icon = "šŸŽ‰" if completed else "āš ļø" - self.event_display.append(f"\n{status_icon} SESSION COMPLETE") - self.event_display.append(f" Total API calls: {total_calls}") - self.event_display.append(f" Status: {'Success' if completed else 'Failed/Incomplete'}") - if final_response: - self.event_display.append(f" Final response: {final_response}...") - self.event_display.append(f"\n{'='*80}\n") - - elif event_type == "error": - error_msg = data.get("error_message", "Unknown error") - self.event_display.append(f"āŒ ERROR") - self.event_display.append(f" {error_msg}") - - else: - # Unknown event type - self.event_display.append(f"āš ļø {event_type.upper()}") - self.event_display.append(f" {json.dumps(data, indent=2)[:200]}...") - - self.event_display.append("") # Blank line - - # Auto-scroll to bottom - cursor = self.event_display.textCursor() - cursor.movePosition(QTextCursor.End) - self.event_display.setTextCursor(cursor) - - -class HermesMainWindow(QMainWindow): - """ - Main window for Hermes Agent UI. - - Provides interface for: - - Submitting queries - - Configuring agent settings - - Viewing real-time events - - Managing sessions - """ - - def __init__(self): - super().__init__() - self.api_base_url = "http://localhost:8000" - self.ws_client = None - self.current_session_id = None - self.available_toolsets = [] - self.is_closing = False # Flag to prevent reconnection during shutdown - - self.init_ui() - self.setup_websocket() - self.load_available_tools() - - def init_ui(self): - """Initialize the user interface.""" - self.setWindowTitle("Hermes Agent - AI Assistant UI") - self.setGeometry(100, 100, 1400, 900) - - # Central widget - central_widget = QWidget() - self.setCentralWidget(central_widget) - - # Main layout (horizontal split) - main_layout = QHBoxLayout() - - # Left panel: Controls - left_panel = self.create_control_panel() - - # Right panel: Event display - right_panel = self.create_event_panel() - - # Splitter for resizable panels - splitter = QSplitter(Qt.Horizontal) - splitter.addWidget(left_panel) - splitter.addWidget(right_panel) - splitter.setStretchFactor(0, 1) # Control panel - splitter.setStretchFactor(1, 2) # Event panel (larger) - - main_layout.addWidget(splitter) - central_widget.setLayout(main_layout) - - # Status bar - self.statusBar().showMessage("Ready") - - def create_control_panel(self) -> QWidget: - """Create the left control panel.""" - panel = QWidget() - layout = QVBoxLayout() - - # Title - title = QLabel("šŸ¤– Hermes Agent Control") - title.setFont(QFont("Arial", 14, QFont.Bold)) - title.setAlignment(Qt.AlignCenter) - layout.addWidget(title) - - # Query input group - query_group = QGroupBox("Query Input") - query_layout = QVBoxLayout() - - self.query_input = QTextEdit() - self.query_input.setPlaceholderText("Enter your query here...") - self.query_input.setMaximumHeight(150) - query_layout.addWidget(self.query_input) - - self.submit_btn = QPushButton("šŸš€ Submit Query") - self.submit_btn.setFont(QFont("Arial", 11, QFont.Bold)) - self.submit_btn.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; padding: 10px; }") - self.submit_btn.clicked.connect(self.submit_query) - query_layout.addWidget(self.submit_btn) - - query_group.setLayout(query_layout) - layout.addWidget(query_group) - - # Model configuration group - model_group = QGroupBox("Model Configuration") - model_layout = QVBoxLayout() - - # Model selection - model_layout.addWidget(QLabel("Model:")) - self.model_combo = QComboBox() - self.model_combo.addItems([ - "claude-sonnet-4-5-20250929", - "claude-opus-4-20250514", - "gpt-4", - "gpt-4-turbo" - ]) - model_layout.addWidget(self.model_combo) - - # API Base URL - model_layout.addWidget(QLabel("API Base URL:")) - self.base_url_input = QLineEdit("https://api.anthropic.com/v1/") - model_layout.addWidget(self.base_url_input) - - # Max turns - model_layout.addWidget(QLabel("Max Turns:")) - self.max_turns_spin = QSpinBox() - self.max_turns_spin.setMinimum(1) - self.max_turns_spin.setMaximum(50) - self.max_turns_spin.setValue(10) - model_layout.addWidget(self.max_turns_spin) - - model_group.setLayout(model_layout) - layout.addWidget(model_group) - - # Tools configuration group - tools_group = QGroupBox("Tools & Toolsets") - tools_layout = QVBoxLayout() - - tools_layout.addWidget(QLabel("Select Toolsets:")) - self.toolsets_list = QListWidget() - self.toolsets_list.setSelectionMode(QListWidget.MultiSelection) - self.toolsets_list.setMaximumHeight(150) - tools_layout.addWidget(self.toolsets_list) - - tools_group.setLayout(tools_layout) - layout.addWidget(tools_group) - - # Options group - options_group = QGroupBox("Options") - options_layout = QVBoxLayout() - - self.mock_mode_checkbox = QCheckBox("Mock Web Tools (Testing)") - options_layout.addWidget(self.mock_mode_checkbox) - - self.verbose_checkbox = QCheckBox("Verbose Logging") - options_layout.addWidget(self.verbose_checkbox) - - options_layout.addWidget(QLabel("Mock Delay (seconds):")) - self.mock_delay_spin = QSpinBox() - self.mock_delay_spin.setMinimum(1) - self.mock_delay_spin.setMaximum(300) - self.mock_delay_spin.setValue(60) - options_layout.addWidget(self.mock_delay_spin) - - options_group.setLayout(options_layout) - layout.addWidget(options_group) - - # Connection status - self.connection_status = QLabel("šŸ”“ Disconnected") - self.connection_status.setAlignment(Qt.AlignCenter) - self.connection_status.setStyleSheet("QLabel { padding: 5px; background-color: #F44336; color: white; border-radius: 3px; }") - layout.addWidget(self.connection_status) - - # Add stretch to push everything to top - layout.addStretch() - - panel.setLayout(layout) - return panel - - def create_event_panel(self) -> QWidget: - """Create the right event display panel.""" - panel = QWidget() - layout = QVBoxLayout() - - # Event display widget - self.event_widget = EventDisplayWidget() - layout.addWidget(self.event_widget) - - panel.setLayout(layout) - return panel - - def setup_websocket(self): - """Setup WebSocket connection for real-time events.""" - self.ws_client = WebSocketClient("ws://localhost:8000/ws") - - # Connect signals - self.ws_client.connected.connect(self.on_ws_connected) - self.ws_client.disconnected.connect(self.on_ws_disconnected) - self.ws_client.error.connect(self.on_ws_error) - self.ws_client.event_received.connect(self.on_event_received) - - # Start connection - self.ws_client.connect() - - @Slot() - def on_ws_connected(self): - """Called when WebSocket connection is established.""" - self.connection_status.setText("🟢 Connected") - self.connection_status.setStyleSheet("QLabel { padding: 5px; background-color: #4CAF50; color: white; border-radius: 3px; }") - self.statusBar().showMessage("WebSocket connected") - - @Slot() - def on_ws_disconnected(self): - """Called when WebSocket connection is lost.""" - # Don't attempt reconnection if we're closing the application - if self.is_closing: - return - - self.connection_status.setText("šŸ”“ Disconnected") - self.connection_status.setStyleSheet("QLabel { padding: 5px; background-color: #F44336; color: white; border-radius: 3px; }") - self.statusBar().showMessage("WebSocket disconnected - attempting reconnect...") - - # Attempt reconnect after 5 seconds - QTimer.singleShot(5000, self.ws_client.connect) - - @Slot(str) - def on_ws_error(self, error: str): - """Called when WebSocket error occurs.""" - self.statusBar().showMessage(f"WebSocket error: {error}") - - @Slot(dict) - def on_event_received(self, event: Dict[str, Any]): - """ - Called when an event is received from WebSocket. - - Args: - event: Event data from server - """ - self.event_widget.add_event(event) - - # Update status for specific events - event_type = event.get("event_type") - if event_type == "query": - self.statusBar().showMessage("Query received - agent processing...") - elif event_type == "complete": - self.statusBar().showMessage("Agent completed!") - self.submit_btn.setEnabled(True) - - def load_available_tools(self): - """Load available toolsets from the API.""" - try: - response = requests.get(f"{self.api_base_url}/tools", timeout=5) - if response.status_code == 200: - data = response.json() - toolsets = data.get("toolsets", []) - - self.available_toolsets = toolsets - self.toolsets_list.clear() - - for toolset in toolsets: - name = toolset.get("name", "") - description = toolset.get("description", "") - tool_count = toolset.get("tool_count", 0) - - item_text = f"{name} ({tool_count} tools) - {description}" - item = QListWidgetItem(item_text) - item.setData(Qt.UserRole, name) # Store toolset name - self.toolsets_list.addItem(item) - - self.statusBar().showMessage(f"Loaded {len(toolsets)} toolsets") - else: - self.statusBar().showMessage("Failed to load toolsets from API") - - except requests.exceptions.RequestException as e: - self.statusBar().showMessage(f"Error loading toolsets: {str(e)}") - # Add some default toolsets - default_toolsets = ["web", "vision", "terminal", "research"] - for ts in default_toolsets: - item = QListWidgetItem(f"{ts} (default)") - item.setData(Qt.UserRole, ts) - self.toolsets_list.addItem(item) - - @Slot() - def submit_query(self): - """Submit query to the agent API.""" - query = self.query_input.toPlainText().strip() - - if not query: - QMessageBox.warning(self, "No Query", "Please enter a query first!") - return - - # Get selected toolsets - selected_toolsets = [] - for i in range(self.toolsets_list.count()): - item = self.toolsets_list.item(i) - if item.isSelected(): - toolset_name = item.data(Qt.UserRole) - selected_toolsets.append(toolset_name) - - # Build request payload - payload = { - "query": query, - "model": self.model_combo.currentText(), - "base_url": self.base_url_input.text(), - "max_turns": self.max_turns_spin.value(), - "enabled_toolsets": selected_toolsets if selected_toolsets else None, - "mock_web_tools": self.mock_mode_checkbox.isChecked(), - "mock_delay": self.mock_delay_spin.value(), - "verbose": self.verbose_checkbox.isChecked() - } - - # Disable submit button during execution - self.submit_btn.setEnabled(False) - self.submit_btn.setText("ā³ Running...") - self.statusBar().showMessage("Submitting query to agent...") - - # Submit to API - try: - response = requests.post( - f"{self.api_base_url}/agent/run", - json=payload, - timeout=10 - ) - - if response.status_code == 200: - result = response.json() - session_id = result.get("session_id", "") - self.current_session_id = session_id - - self.statusBar().showMessage(f"Agent started! Session: {session_id[:8]}...") - - # Clear event display for new session - # (or keep history - user preference) - # self.event_widget.clear_events() - - else: - QMessageBox.warning( - self, - "API Error", - f"Failed to start agent: {response.status_code}\n{response.text}" - ) - self.submit_btn.setEnabled(True) - self.submit_btn.setText("šŸš€ Submit Query") - - except requests.exceptions.RequestException as e: - QMessageBox.critical( - self, - "Connection Error", - f"Failed to connect to API server:\n{str(e)}\n\nMake sure the server is running:\npython logging_server.py" - ) - self.submit_btn.setEnabled(True) - self.submit_btn.setText("šŸš€ Submit Query") - - # Re-enable button after short delay (UI feedback) - QTimer.singleShot(2000, lambda: self.submit_btn.setText("šŸš€ Submit Query")) - - def cleanup(self): - """Clean up resources before exit.""" - print("Cleaning up resources...") - self.is_closing = True - - if self.ws_client: - try: - self.ws_client.disconnect() - except Exception as e: - print(f"Error disconnecting WebSocket: {e}") - - def closeEvent(self, event): - """Handle window close event - ensures clean shutdown.""" - print("Closing application...") - self.cleanup() - event.accept() +from main_window import HermesMainWindow def setup_signal_handlers(app: QApplication) -> QTimer: @@ -636,7 +50,6 @@ def setup_signal_handlers(app: QApplication) -> QTimer: print("\nšŸ›‘ Interrupt received, shutting down gracefully...") app.quit() - # Register signal handlers signal.signal(signal.SIGINT, signal_handler) # Ctrl+C signal.signal(signal.SIGTERM, signal_handler) # Termination signal diff --git a/ui/main_window.py b/ui/main_window.py new file mode 100644 index 0000000000..c879a57c8f --- /dev/null +++ b/ui/main_window.py @@ -0,0 +1,375 @@ +""" +Main window for Hermes Agent UI. + +This module provides the main application window with controls for +submitting queries, configuring settings, and viewing real-time events. +""" + +import requests +from typing import Dict, Any + +from PySide6.QtWidgets import ( + QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, + QPushButton, QLabel, QLineEdit, QComboBox, QCheckBox, + QGroupBox, QSplitter, QListWidget, QListWidgetItem, + QSpinBox, QMessageBox +) +from PySide6.QtCore import Qt, Slot, QTimer +from PySide6.QtGui import QFont + +from .websocket_client import WebSocketClient +from .event_widgets import InteractiveEventDisplayWidget + + +class HermesMainWindow(QMainWindow): + """ + Main window for Hermes Agent UI. + + Provides interface for: + - Submitting queries + - Configuring agent settings + - Viewing real-time events + - Managing sessions + """ + + def __init__(self): + super().__init__() + self.api_base_url = "http://localhost:8000" + self.ws_client = None + self.current_session_id = None + self.available_toolsets = [] + self.is_closing = False # Flag to prevent reconnection during shutdown + + self.init_ui() + self.setup_websocket() + self.load_available_tools() + + def init_ui(self): + """Initialize the user interface.""" + self.setWindowTitle("Hermes Agent - AI Assistant UI") + self.setGeometry(100, 100, 1400, 900) + + # Central widget + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # Main layout (horizontal split) + main_layout = QHBoxLayout() + + # Left panel: Controls + left_panel = self.create_control_panel() + + # Right panel: Event display + right_panel = self.create_event_panel() + + # Splitter for resizable panels + splitter = QSplitter(Qt.Horizontal) + splitter.addWidget(left_panel) + splitter.addWidget(right_panel) + splitter.setStretchFactor(0, 1) # Control panel + splitter.setStretchFactor(1, 2) # Event panel (larger) + + main_layout.addWidget(splitter) + central_widget.setLayout(main_layout) + + # Status bar + self.statusBar().showMessage("Ready") + + def create_control_panel(self) -> QWidget: + """Create the left control panel.""" + panel = QWidget() + layout = QVBoxLayout() + + # Title + title = QLabel("šŸ¤– Hermes Agent Control") + title.setFont(QFont("Arial", 14, QFont.Bold)) + title.setAlignment(Qt.AlignCenter) + layout.addWidget(title) + + # Query input group + query_group = QGroupBox("Query Input") + query_layout = QVBoxLayout() + + self.query_input = QTextEdit() + self.query_input.setPlaceholderText("Enter your query here...") + self.query_input.setMaximumHeight(150) + query_layout.addWidget(self.query_input) + + self.submit_btn = QPushButton("šŸš€ Submit Query") + self.submit_btn.setFont(QFont("Arial", 11, QFont.Bold)) + self.submit_btn.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; padding: 10px; }") + self.submit_btn.clicked.connect(self.submit_query) + query_layout.addWidget(self.submit_btn) + + query_group.setLayout(query_layout) + layout.addWidget(query_group) + + # Model configuration group + model_group = QGroupBox("Model Configuration") + model_layout = QVBoxLayout() + + # Model selection + model_layout.addWidget(QLabel("Model:")) + self.model_combo = QComboBox() + self.model_combo.addItems([ + "claude-sonnet-4-5-20250929", + "claude-opus-4-20250514", + "gpt-4", + "gpt-4-turbo" + ]) + model_layout.addWidget(self.model_combo) + + # API Base URL + model_layout.addWidget(QLabel("API Base URL:")) + self.base_url_input = QLineEdit("https://api.anthropic.com/v1/") + model_layout.addWidget(self.base_url_input) + + # Max turns + model_layout.addWidget(QLabel("Max Turns:")) + self.max_turns_spin = QSpinBox() + self.max_turns_spin.setMinimum(1) + self.max_turns_spin.setMaximum(50) + self.max_turns_spin.setValue(10) + model_layout.addWidget(self.max_turns_spin) + + model_group.setLayout(model_layout) + layout.addWidget(model_group) + + # Tools configuration group + tools_group = QGroupBox("Tools & Toolsets") + tools_layout = QVBoxLayout() + + tools_layout.addWidget(QLabel("Select Toolsets:")) + self.toolsets_list = QListWidget() + self.toolsets_list.setSelectionMode(QListWidget.MultiSelection) + self.toolsets_list.setMaximumHeight(150) + tools_layout.addWidget(self.toolsets_list) + + tools_group.setLayout(tools_layout) + layout.addWidget(tools_group) + + # Options group + options_group = QGroupBox("Options") + options_layout = QVBoxLayout() + + self.mock_mode_checkbox = QCheckBox("Mock Web Tools (Testing)") + options_layout.addWidget(self.mock_mode_checkbox) + + self.verbose_checkbox = QCheckBox("Verbose Logging") + options_layout.addWidget(self.verbose_checkbox) + + options_layout.addWidget(QLabel("Mock Delay (seconds):")) + self.mock_delay_spin = QSpinBox() + self.mock_delay_spin.setMinimum(1) + self.mock_delay_spin.setMaximum(300) + self.mock_delay_spin.setValue(60) + options_layout.addWidget(self.mock_delay_spin) + + options_group.setLayout(options_layout) + layout.addWidget(options_group) + + # Connection status + self.connection_status = QLabel("šŸ”“ Disconnected") + self.connection_status.setAlignment(Qt.AlignCenter) + self.connection_status.setStyleSheet("QLabel { padding: 5px; background-color: #F44336; color: white; border-radius: 3px; }") + layout.addWidget(self.connection_status) + + # Add stretch to push everything to top + layout.addStretch() + + panel.setLayout(layout) + return panel + + def create_event_panel(self) -> QWidget: + """Create the right event display panel.""" + panel = QWidget() + layout = QVBoxLayout() + + # Event display widget + self.event_widget = InteractiveEventDisplayWidget() + layout.addWidget(self.event_widget) + + panel.setLayout(layout) + return panel + + def setup_websocket(self): + """Setup WebSocket connection for real-time events.""" + self.ws_client = WebSocketClient("ws://localhost:8000/ws") + + # Connect signals + self.ws_client.connected.connect(self.on_ws_connected) + self.ws_client.disconnected.connect(self.on_ws_disconnected) + self.ws_client.error.connect(self.on_ws_error) + self.ws_client.event_received.connect(self.on_event_received) + + # Start connection + self.ws_client.connect() + + @Slot() + def on_ws_connected(self): + """Called when WebSocket connection is established.""" + self.connection_status.setText("🟢 Connected") + self.connection_status.setStyleSheet("QLabel { padding: 5px; background-color: #4CAF50; color: white; border-radius: 3px; }") + self.statusBar().showMessage("WebSocket connected") + + @Slot() + def on_ws_disconnected(self): + """Called when WebSocket connection is lost.""" + # Don't attempt reconnection if we're closing the application + if self.is_closing: + return + + self.connection_status.setText("šŸ”“ Disconnected") + self.connection_status.setStyleSheet("QLabel { padding: 5px; background-color: #F44336; color: white; border-radius: 3px; }") + self.statusBar().showMessage("WebSocket disconnected - attempting reconnect...") + + # Attempt reconnect after 5 seconds + QTimer.singleShot(5000, self.ws_client.connect) + + @Slot(str) + def on_ws_error(self, error: str): + """Called when WebSocket error occurs.""" + self.statusBar().showMessage(f"WebSocket error: {error}") + + @Slot(dict) + def on_event_received(self, event: Dict[str, Any]): + """ + Called when an event is received from WebSocket. + + Args: + event: Event data from server + """ + self.event_widget.add_event(event) + + # Update status for specific events + event_type = event.get("event_type") + if event_type == "query": + self.statusBar().showMessage("Query received - agent processing...") + elif event_type == "complete": + self.statusBar().showMessage("Agent completed!") + self.submit_btn.setEnabled(True) + + def load_available_tools(self): + """Load available toolsets from the API.""" + try: + response = requests.get(f"{self.api_base_url}/tools", timeout=5) + if response.status_code == 200: + data = response.json() + toolsets = data.get("toolsets", []) + + self.available_toolsets = toolsets + self.toolsets_list.clear() + + for toolset in toolsets: + name = toolset.get("name", "") + description = toolset.get("description", "") + tool_count = toolset.get("tool_count", 0) + + item_text = f"{name} ({tool_count} tools) - {description}" + item = QListWidgetItem(item_text) + item.setData(Qt.UserRole, name) # Store toolset name + self.toolsets_list.addItem(item) + + self.statusBar().showMessage(f"Loaded {len(toolsets)} toolsets") + else: + self.statusBar().showMessage("Failed to load toolsets from API") + + except requests.exceptions.RequestException as e: + self.statusBar().showMessage(f"Error loading toolsets: {str(e)}") + # Add some default toolsets + default_toolsets = ["web", "vision", "terminal", "research"] + for ts in default_toolsets: + item = QListWidgetItem(f"{ts} (default)") + item.setData(Qt.UserRole, ts) + self.toolsets_list.addItem(item) + + @Slot() + def submit_query(self): + """Submit query to the agent API.""" + query = self.query_input.toPlainText().strip() + + if not query: + QMessageBox.warning(self, "No Query", "Please enter a query first!") + return + + # Get selected toolsets + selected_toolsets = [] + for i in range(self.toolsets_list.count()): + item = self.toolsets_list.item(i) + if item.isSelected(): + toolset_name = item.data(Qt.UserRole) + selected_toolsets.append(toolset_name) + + # Build request payload + payload = { + "query": query, + "model": self.model_combo.currentText(), + "base_url": self.base_url_input.text(), + "max_turns": self.max_turns_spin.value(), + "enabled_toolsets": selected_toolsets if selected_toolsets else None, + "mock_web_tools": self.mock_mode_checkbox.isChecked(), + "mock_delay": self.mock_delay_spin.value(), + "verbose": self.verbose_checkbox.isChecked() + } + + # Disable submit button during execution + self.submit_btn.setEnabled(False) + self.submit_btn.setText("ā³ Running...") + self.statusBar().showMessage("Submitting query to agent...") + + # Submit to API + try: + response = requests.post( + f"{self.api_base_url}/agent/run", + json=payload, + timeout=10 + ) + + if response.status_code == 200: + result = response.json() + session_id = result.get("session_id", "") + self.current_session_id = session_id + + self.statusBar().showMessage(f"Agent started! Session: {session_id[:8]}...") + + # Clear event display for new session (optional) + # self.event_widget.clear_events() + + else: + QMessageBox.warning( + self, + "API Error", + f"Failed to start agent: {response.status_code}\n{response.text}" + ) + self.submit_btn.setEnabled(True) + self.submit_btn.setText("šŸš€ Submit Query") + + except requests.exceptions.RequestException as e: + QMessageBox.critical( + self, + "Connection Error", + f"Failed to connect to API server:\n{str(e)}\n\nMake sure the server is running:\npython logging_server.py" + ) + self.submit_btn.setEnabled(True) + self.submit_btn.setText("šŸš€ Submit Query") + + # Re-enable button after short delay (UI feedback) + QTimer.singleShot(2000, lambda: self.submit_btn.setText("šŸš€ Submit Query")) + + def cleanup(self): + """Clean up resources before exit.""" + print("Cleaning up resources...") + self.is_closing = True + + if self.ws_client: + try: + self.ws_client.disconnect() + except Exception as e: + print(f"Error disconnecting WebSocket: {e}") + + def closeEvent(self, event): + """Handle window close event - ensures clean shutdown.""" + print("Closing application...") + self.cleanup() + event.accept() + diff --git a/ui/websocket_client.py b/ui/websocket_client.py new file mode 100644 index 0000000000..3136c52e17 --- /dev/null +++ b/ui/websocket_client.py @@ -0,0 +1,91 @@ +""" +WebSocket client for real-time event streaming from Hermes Agent. + +This module provides a WebSocket client that runs in a separate thread +and emits Qt signals when events are received from the server. +""" + +import json +import threading +import websocket +from PySide6.QtCore import QObject, Signal + + +class WebSocketClient(QObject): + """ + WebSocket client for receiving real-time agent events. + + Runs in a separate thread and emits Qt signals when events arrive. + """ + + # Signals for event communication + event_received = Signal(dict) # Emits parsed event data + connected = Signal() + disconnected = Signal() + error = Signal(str) + + def __init__(self, url: str = "ws://localhost:8000/ws"): + super().__init__() + self.url = url + self.ws = None + self.running = False + self.thread = None + + def connect(self): + """Start WebSocket connection in background thread.""" + if self.running: + return + + self.running = True + self.thread = threading.Thread(target=self._run, daemon=True) + self.thread.start() + + def disconnect(self): + """Stop WebSocket connection.""" + self.running = False + if self.ws: + try: + self.ws.close() + except Exception as e: + print(f"Error closing WebSocket: {e}") + + def _run(self): + """WebSocket event loop (runs in background thread).""" + try: + self.ws = websocket.WebSocketApp( + self.url, + on_open=self._on_open, + on_message=self._on_message, + on_error=self._on_error, + on_close=self._on_close + ) + + # Run forever with reconnection + self.ws.run_forever(ping_interval=300, ping_timeout=60) + + except Exception as e: + self.error.emit(f"WebSocket error: {str(e)}") + + def _on_open(self, ws): + """Called when WebSocket connection is established.""" + print("WebSocket connected") + self.connected.emit() + + def _on_message(self, ws, message): + """Called when a message is received from the server.""" + try: + data = json.loads(message) + self.event_received.emit(data) + except json.JSONDecodeError as e: + print(f" Failed to parse WebSocket message: {e}") + + def _on_error(self, ws, error): + """Called when an error occurs.""" + print(f"WebSocket error: {error}") + self.error.emit(str(error)) + + def _on_close(self, ws, close_status_code, close_msg): + """Called when WebSocket connection is closed.""" + print(f"šŸ”Œ WebSocket disconnected: {close_status_code} - {close_msg}") + self.disconnected.emit() +