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">

Analysis Complete

-
+
{% for cat, val in result.dict().items() %} {% if not loop.last %} diff --git a/src/frontend/templates/status.html b/src/frontend/templates/status.html index c3e5255..f78f912 100644 --- a/src/frontend/templates/status.html +++ b/src/frontend/templates/status.html @@ -9,18 +9,25 @@
{% if is_error %} {% set bg_color = 'danger' %} - {% set header_content, content = message.split('body:', 1) %} - {% elif is_loading %} - {% set bg_color = 'success' %} + {% set header_content = 'Error' %} {% set content = message %} - {% set header_content = 'Complete' %} - {% elif is_tool_message %} - {% set bg_color = 'dark'%} - {% set header_content, content = message.split(' ', 2)[2].split('...', 1) %} - {% else %} - {% set bg_color = 'info' %} + {% elif is_loading and "analysis complete" not in message.lower() %} + {% set bg_color = 'warning' %} {% set content = message %} {% set header_content = 'In Progress' %} + + {% elif is_tool_message and "analysis complete" not in message.lower() %} + {% set bg_color = 'info '%} + {% set header_content, content = message.split(' ', 2)[2].split('...', 1) %} + + {% elif "analyzing..." in message.lower() %} + {% set bg_color = 'link' %} + {% set content = 'message' %} + {% set header_content = "Starting"%} + {% else %} + {% set bg_color = 'success' %} + {% set content = message %} + {% set header_content = 'Done!' %} {% endif %} {{ message }} diff --git a/uv.lock b/uv.lock index 732d698..e871ece 100644 --- a/uv.lock +++ b/uv.lock @@ -282,7 +282,7 @@ requires-dist = [ { name = "aiomysql", specifier = "==0.2.0" }, { name = "amqp", specifier = "==5.3.1" }, { name = "annotated-types", specifier = "==0.7.0" }, - { name = "anthropic", specifier = "==0.44.0" }, + { name = "anthropic", specifier = "==0.45.0" }, { name = "anyio", specifier = "==4.8.0" }, { name = "appdirs", specifier = "==1.4.4" }, { name = "asgiref", specifier = "==3.8.1" }, @@ -310,7 +310,7 @@ requires-dist = [ { name = "fastapi-restful", specifier = "==0.6.0" }, { name = "fastcrud", specifier = "==0.15.5" }, { name = "flower", specifier = "==2.0.1" }, - { name = "google-auth", specifier = "==2.37.0" }, + { name = "google-auth", specifier = "==2.38.0" }, { name = "googleapis-common-protos", specifier = "==1.66.0" }, { name = "greenlet", specifier = "==3.1.1" }, { name = "griffe", specifier = "==1.5.5" }, @@ -510,7 +510,7 @@ dev = [ { name = "pydantic-extra-types", specifier = "==2.10.2" }, { name = "pydantic-settings", specifier = "==2.7.1" }, { name = "pygments", specifier = "==2.19.1" }, - { name = "pyinstrument", specifier = "==5.0.0" }, + { name = "pyinstrument", specifier = "==5.0.1" }, { name = "python-dateutil", specifier = "==2.9.0.post0" }, { name = "python-dotenv", specifier = "==1.0.1" }, { name = "python-json-logger", specifier = "==3.2.1" }, @@ -606,7 +606,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.44.0" +version = "0.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -617,9 +617,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/34/ed394012684f7d6e36bb36c8b7c82b438f0ef189d2afd3d0090b85801211/anthropic-0.44.0.tar.gz", hash = "sha256:dc5c91c8b0463f97513d2e79350511ef295a56910bac4fbbe9491016c71f2ef0", size = 196065 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/d4/51d5ed9159e53d51012c7ffa2b578f1c76cc9bd2d0646695604a20129b3c/anthropic-0.45.0.tar.gz", hash = "sha256:4e8541dc355332090bfc51b84549c19b649a13a23dbd6bd68e1d012e08551025", size = 200523 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/ad/c3c7d199eedc0b6d4621deed8dc1ed252ce399400cd76f1da098f1f50e56/anthropic-0.44.0-py3-none-any.whl", hash = "sha256:7087ccfc8ed7b164f971e094495cd3aeabac1e435fa393480cc146c87946c21c", size = 208634 }, + { url = "https://files.pythonhosted.org/packages/63/a0/f8ad781d83ccd8184692ace3c557294b1ece7c0b071f912300276b7cdb4a/anthropic-0.45.0-py3-none-any.whl", hash = "sha256:f36aff71d2c232945e64d1970be68a91b05a2ef5e3afa6c1ff195c3303a95ad3", size = 222255 }, ] [[package]] @@ -1196,16 +1196,16 @@ wheels = [ [[package]] name = "google-auth" -version = "2.37.0" +version = "2.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/af/b25763b9d35dfc2c6f9c3ec34d8d3f1ba760af3a7b7e8d5c5f0579522c45/google_auth-2.37.0.tar.gz", hash = "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00", size = 268878 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/8d/4d5d5f9f500499f7bd4c93903b43e8d6976f3fc6f064637ded1a85d09b07/google_auth-2.37.0-py2.py3-none-any.whl", hash = "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0", size = 209829 }, + { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770 }, ] [[package]] @@ -2677,20 +2677,20 @@ wheels = [ [[package]] name = "pyinstrument" -version = "5.0.0" +version = "5.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/14/726f2e2553aca08f25b7166197d22a4426053d5fb423c53417342ac584b1/pyinstrument-5.0.0.tar.gz", hash = "sha256:144f98eb3086667ece461f66324bf1cc1ee0475b399ab3f9ded8449cc76b7c90", size = 262211 } +sdist = { url = "https://files.pythonhosted.org/packages/64/6e/85c2722e40cab4fd9df6bbe68a0d032e237cf8cfada71e5f067e4e433214/pyinstrument-5.0.1.tar.gz", hash = "sha256:f4fd0754d02959c113a4b1ebed02f4627b6e2c138719ddf43244fd95f201c8c9", size = 263162 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/c9/b2ed3db062bca45decb7fdcab2ed2cba6b1afb32b21bbde7166aafe5ecd3/pyinstrument-5.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79a54def2d4aa83a4ed37c6cffc5494ae5de140f0453169eb4f7c744cc249d3a", size = 128268 }, - { url = "https://files.pythonhosted.org/packages/0f/14/456f51598c2e8401b248c38591488c3815f38a4c0bca6babb3f81ab93a71/pyinstrument-5.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9538f746f166a40c8802ebe5c3e905d50f3faa189869cd71c083b8a639e574bb", size = 120299 }, - { url = "https://files.pythonhosted.org/packages/11/e8/abeecedfa5dc6e6651e569c8876f0a55b973c906ebeb90185504a792ddb2/pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbab65cae1483ad8a18429511d1eac9e3efec9f7961f2fd1bf90e1e2d69ef15", size = 143953 }, - { url = "https://files.pythonhosted.org/packages/80/03/107d3889ea42a777b0231bf3b8e5da8f8370b5bed5a55d79bcf7607d2393/pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4351ad041d208c597e296a0e9c2e6e21cc96804608bcafa40cfa168f3c2b8f79", size = 142858 }, - { url = "https://files.pythonhosted.org/packages/72/6c/0f4af16e529d0ea290cbc72f97e0403a118692f954b2abdaf5547e05e026/pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceee5252f4580abec29bcc5c965453c217b0d387c412a5ffb8afdcda4e648feb", size = 144259 }, - { url = "https://files.pythonhosted.org/packages/18/c7/1a8100197b67c03a8a733d0ffbc881c35f23ccbaf0f0e470c03b0e639da5/pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3050a4e7033103a13cfff9802680e2070a9173e1a258fa3f15a80b4eb9ee278", size = 143951 }, - { url = "https://files.pythonhosted.org/packages/87/bb/9826f6a62f2fee88a54059e1ca36a9766dab6220f826c8745dc453c31e99/pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3b1f44a34da7810938df615fb7cbc43cd879b42ca6b5cd72e655aee92149d012", size = 143722 }, - { url = "https://files.pythonhosted.org/packages/42/2c/9a5b0cc42296637e23f50881e36add73edde2e668d34095e3ddbd899a1e6/pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fde075196c8a3b2be191b8da05b92ff909c78d308f82df56d01a8cfdd6da07b9", size = 144138 }, - { url = "https://files.pythonhosted.org/packages/66/96/85044622fae98feaabaf26dbee39a7151d9a7c8d020a870033cd90f326ca/pyinstrument-5.0.0-cp313-cp313-win32.whl", hash = "sha256:1a9b62a8b54e05e7723eb8b9595fadc43559b73290c87b3b1cb2dc5944559790", size = 121977 }, - { url = "https://files.pythonhosted.org/packages/dd/36/a6a44b5162a9d102b085ef7107299be766868679ab2c974a4888823c8a0f/pyinstrument-5.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2478d2c55f77ad8e281e67b0dfe7c2176304bb824c307e86e11890f5e68d7feb", size = 122766 }, + { url = "https://files.pythonhosted.org/packages/0f/ae/f8f84ecd0dc2c4f0d84920cb4ffdbea52a66e4b4abc2110f18879b57f538/pyinstrument-5.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f5065639dfedc3b8e537161f9aaa8c550c8717c935a962e9bf1e843bf0e8791f", size = 128900 }, + { url = "https://files.pythonhosted.org/packages/23/2f/b742c46d86d4c1f74ec0819f091bbc2fad0bab786584a18d89d9178802f1/pyinstrument-5.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b5d20802b0c2bd1ddb95b2e96ebd3e9757dbab1e935792c2629166f1eb267bb2", size = 121445 }, + { url = "https://files.pythonhosted.org/packages/d9/e0/297dc8454ed437aec0fbdc3cc1a6a5fdf6701935b91dd31caf38c5e3ff92/pyinstrument-5.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6f5655d580429e7992c37757cc5f6e74ca81b0f2768b833d9711631a8cb2f7", size = 144904 }, + { url = "https://files.pythonhosted.org/packages/8b/df/e4faff09fdbad7e685ceb0f96066d434fc8350382acf8df47577653f702b/pyinstrument-5.0.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4c8c9ad93f62f0bf2ddc7fb6fce3a91c008d422873824e01c5e5e83467fd1fb", size = 143801 }, + { url = "https://files.pythonhosted.org/packages/b1/63/ed2955d980bbebf17155119e2687ac15e170b6221c4bb5f5c37f41323fe5/pyinstrument-5.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db15d1854b360182d242da8de89761a0ffb885eea61cb8652e40b5b9a4ef44bc", size = 145204 }, + { url = "https://files.pythonhosted.org/packages/c4/18/31b8dcdade9767afc7a36a313d8cf9c5690b662e9755fe7bd0523125e06f/pyinstrument-5.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c803f7b880394b7bba5939ff8a59d6962589e9a0140fc33c3a6a345c58846106", size = 144881 }, + { url = "https://files.pythonhosted.org/packages/1f/14/cd19894eb03dd28093f564e8bcf7ae4edc8e315ce962c8155cf795fc0784/pyinstrument-5.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:84e37ffabcf26fe820d354a1f7e9fc26949f953addab89b590c5000b3ffa60d0", size = 144643 }, + { url = "https://files.pythonhosted.org/packages/80/54/3dd08f5a869d3b654ff7e4e4c9d2b34f8de73fb0f2f792fac5024a312e0f/pyinstrument-5.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a0d23d3763ec95da0beb390c2f7df7cbe36ea62b6a4d5b89c4eaab81c1c649cf", size = 145070 }, + { url = "https://files.pythonhosted.org/packages/5d/dc/ac8e798235a1dbccefc1b204a16709cef36f02c07587763ba8eb510fc8bc/pyinstrument-5.0.1-cp313-cp313-win32.whl", hash = "sha256:967f84bd82f14425543a983956ff9cfcf1e3762755ffcec8cd835c6be22a7a0a", size = 123030 }, + { url = "https://files.pythonhosted.org/packages/52/59/adcb3e85c9105c59382723a67f682012aa7f49027e270e721f2d59f63fcf/pyinstrument-5.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:70b16b5915534d8df40dcf04a7cc78d3290464c06fa358a4bc324280af4c74e0", size = 123825 }, ] [[package]]