diff --git a/core_requirements.txt b/core_requirements.txt index 0223b1d..f8669b2 100644 --- a/core_requirements.txt +++ b/core_requirements.txt @@ -10,7 +10,7 @@ annotated-types==0.7.0 # via # pydantic # sqlmodel-crud-utilities -anthropic==0.44.0 +anthropic==0.45.0 # via pydantic-ai-slim anyio==4.8.0 # via @@ -101,7 +101,7 @@ fastcrud==0.15.5 # via -r core_requirements.in flower==2.0.1 # via -r core_requirements.in -google-auth==2.37.0 +google-auth==2.38.0 # via pydantic-ai-slim googleapis-common-protos==1.66.0 # via opentelemetry-exporter-otlp-proto-http diff --git a/dev_requirements.txt b/dev_requirements.txt index 5478c30..e96343f 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -273,7 +273,7 @@ pygments==2.19.1 # -c core_requirements.txt # ipython # nbconvert -pyinstrument==5.0.0 +pyinstrument==5.0.1 # via fastapi-debug-toolbar python-dateutil==2.9.0.post0 # via diff --git a/pyproject.toml b/pyproject.toml index d6dfd71..fd97f73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dependencies = [ "aiomysql==0.2.0", "amqp==5.3.1", "annotated-types==0.7.0", - "anthropic==0.44.0", + "anthropic==0.45.0", "anyio==4.8.0", "appdirs==1.4.4", "asgiref==3.8.1", @@ -108,7 +108,7 @@ dependencies = [ "fastapi==0.115.7", "fastcrud==0.15.5", "flower==2.0.1", - "google-auth==2.37.0", + "google-auth==2.38.0", "googleapis-common-protos==1.66.0", "greenlet==3.1.1", "griffe==1.5.5", @@ -308,7 +308,7 @@ dev = [ "pydantic-settings==2.7.1", "pydantic==2.10.5", "pygments==2.19.1", - "pyinstrument==5.0.0", + "pyinstrument==5.0.1", "python-dateutil==2.9.0.post0", "python-dotenv==1.0.1", "python-json-logger==3.2.1", diff --git a/src/backend/core/tools.py b/src/backend/core/tools.py index 460d307..f4ee1b9 100644 --- a/src/backend/core/tools.py +++ b/src/backend/core/tools.py @@ -1,12 +1,14 @@ +import asyncio + import httpx from bs4 import BeautifulSoup as soup -from pydantic_ai import RunContext +from pydantic_ai import ModelRetry, RunContext from backend.core.consts import AI_MODEL from backend.core.core import SwotAgentDeps, SwotAnalysis, swot_agent from backend.core.utils import report_tool_usage from backend.logger import logger -from backend.utils import get_val +from backend.utils import get_val, set_event_loop, windows_sys_event_loop_check @swot_agent.tool(prepare=report_tool_usage) @@ -76,7 +78,7 @@ async def analyze_competition( async def get_reddit_insights( ctx: RunContext[SwotAgentDeps], query: str, - subreddit_name: str | None = None, + subreddit_names: list[str] | None = None, ): """ A tool to gain insights from a subreddit. Data is returned as string @@ -84,25 +86,87 @@ async def get_reddit_insights( :param ctx: RunContext[SwotAgentDeps] :param query: str - :param subreddit_name: str + :param subreddit_names: str :return: str """ - if not subreddit_name: - subreddit_name = get_val("REDDIT_SUBREDDIT", "python") - subreddit = ctx.deps.reddit_client.subreddit(subreddit_name) - search_results = subreddit.search(query) - + if not subreddit_names: + subreddit_names = get_val("REDDIT_SUBREDDIT", "python, ") + subreddit_names = [x.strip() for x in subreddit_names.split(",")] insights = [] - for post in search_results: - insights.append( - f"Title: {post.title}\n" - f"URL: {post.url}\n" - f"Content: {post.selftext}\n", - ) + if len(subreddit_names) <= 3: + for name in subreddit_names: + subreddit = ctx.deps.reddit_client.subreddit(name) + search_results = subreddit.search(query) + + for post in search_results: + insights.append( + f"Title: {post.title}\n" + f"URL: {post.url}\n" + f"Content: {post.selftext}\n", + ) + else: + windows_sys_event_loop_check() + set_event_loop() + loop = asyncio.get_event_loop() + tasks = [ + asyncio.ensure_future( + loop.run_in_executor( + None, ctx.deps.reddit_client.subreddit(name).search, query + ) + ) + for name in subreddit_names + ] + results = await asyncio.gather(*tasks) + for result in results: + for post in result: + insights.append( + f"Title: {post.title}\n" + f"URL: {post.url}\n" + f"Content: {post.selftext}\n", + ) return "\n".join(insights) +@swot_agent.result_validator +def validate_result( + _ctx: RunContext[SwotAgentDeps], value: SwotAnalysis +) -> SwotAnalysis: + """ + A validator for SWOT Analysis results; provides greater completeness and + quality control + :param _ctx: RunContext[SwotAgentDeps] + :param value: SwotAnalysis + :return: SwotAnalysis + """ + issues = [] + min = 2 + categories = { + k.title(): getattr(value, k) + for k in ("strengths", "weaknesses", "opportunities", "threats") + } + + for cat_name, points in categories.items(): + if len(points) < min: + issues.append( + f"{cat_name} should have at least {min} points. " + f"Current count is {len(points)}." + ) + + min_len_analysis = 100 + if len(value.analysis) < min_len_analysis: + issues.append( + f"Analysis should have at least {min_len_analysis} " + f"characters. Current count is {len(value.analysis)}." + ) + + if issues: + logger.info(f"Validation issues: {issues}") + raise ModelRetry("\n".join(issues)) + + return value + + async def run_agent( url: str, deps: SwotAgentDeps = SwotAgentDeps(), diff --git a/src/backend/utils.py b/src/backend/utils.py index b6b863a..10831a8 100644 --- a/src/backend/utils.py +++ b/src/backend/utils.py @@ -1,4 +1,6 @@ +import asyncio import os +import sys from decouple import config @@ -92,3 +94,24 @@ def get_val(val: str, default: str | int | bool | None = None, **kwargs): ) return val + + +def windows_sys_event_loop_check(): + """ + A function that sets the event loop policy to a Windows-specific one. + This is a workaround to a known bug involving capturing an existing + asyncio event loop on non-Linux platforms. + """ + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + +def set_event_loop(): + """ + A utility function to capture the existing event loop if it's running. + If no event loop is running, then a new one is created and set. + """ + try: + asyncio.get_running_loop() + except RuntimeError: + asyncio.set_event_loop(asyncio.new_event_loop()) diff --git a/src/frontend/static/css/pygentic_ai.css b/src/frontend/static/css/pygentic_ai.css index a8b50a8..51a5a34 100644 --- a/src/frontend/static/css/pygentic_ai.css +++ b/src/frontend/static/css/pygentic_ai.css @@ -25,7 +25,16 @@ from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + +} + +@media (max-width:768px) { + .cell.is-col-span-2 { + grid-column: span 1; + } + } diff --git a/src/frontend/templates/result.html b/src/frontend/templates/result.html index 000215b..cc14c94 100644 --- a/src/frontend/templates/result.html +++ b/src/frontend/templates/result.html @@ -3,7 +3,7 @@ id="result-container">