enhancements: reddit intelligence gathering now ingests multiple subreddits, as defined by the comma-delimited REDDIT_SUBREDDIT env var; added validate_result validator to swot agent; frontend fixes

This commit is contained in:
Francis Secada 2025-01-23 17:02:56 -05:00
parent ad10d98668
commit f46479bcc4
9 changed files with 155 additions and 52 deletions

View File

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

View File

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

View File

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

View File

@ -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(),

View File

@ -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())

View File

@ -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;
}
}

View File

@ -3,7 +3,7 @@
id="result-container">
<div class="container">
<h2 class="subtitle is-2">Analysis Complete</h2>
<div class="fixed-grid has-2-cols">
<div class="fixed-grid has-1-cols-mobile">
<div class="grid">
{% for cat, val in result.dict().items() %}
{% if not loop.last %}

View File

@ -9,18 +9,25 @@
<div class="box">
{% 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 %}
<StatusResult div_class={{ bg_color }}
header_content={{ header_content }}>{{ message }}

42
uv.lock generated
View File

@ -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]]