Skip to content

feat: add intelligence radar backend#337

Merged
jerry609 merged 3 commits into
devfrom
pr/intelligence-radar-backend
Mar 11, 2026
Merged

feat: add intelligence radar backend#337
jerry609 merged 3 commits into
devfrom
pr/intelligence-radar-backend

Conversation

@jerry609

Copy link
Copy Markdown
Owner

Summary

  • add the intelligence radar backend route, persistence layer, migration, and service implementation
  • add external source clients and source-registry support for community signal collection
  • cover the new backend behavior with route, service, store, and registry tests

Validation

  • pytest tests/unit/test_intelligence_radar_service.py tests/unit/test_intelligence_routes.py tests/unit/test_intelligence_store.py tests/unit/test_source_registry_modes.py -q

Notes

@vercel

vercel Bot commented Mar 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
paper-bot Ready Ready Preview, Comment Mar 11, 2026 9:51am

Copilot AI review requested due to automatic review settings March 11, 2026 07:19
@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b3070140-0d29-45f4-b8e0-0868145213ee

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pr/intelligence-radar-backend

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces the foundational backend components for a new intelligence radar feature. It establishes the necessary database schema, API endpoints, and core service logic to aggregate and present relevant community signals from diverse external platforms. The changes enable the system to proactively identify and surface important developments based on user-defined interests, laying the groundwork for a dynamic and personalized intelligence feed.

Highlights

  • New Intelligence Radar Backend: Introduced a comprehensive backend for an 'intelligence radar' feature, including a new database table, API routes, and service logic to collect and process community signals.
  • External Source Integration: Added support for collecting signals from various external sources such as Reddit, GitHub, HuggingFace Daily Papers, and X (formerly Twitter), enhancing the breadth of intelligence gathering.
  • Persistence Layer and Migration: Implemented a new intelligence_events table and an Alembic migration script to store cached signals, along with a dedicated IntelligenceStore for data management.
  • API Endpoints and Service Logic: Exposed a new /intelligence/feed API endpoint via FastAPI, backed by an IntelligenceRadarService responsible for fetching, scoring, and filtering signals based on user profiles and keywords.
  • Configuration and Environment Variables: Added new environment variables to configure intelligence radar behavior, including API tokens, keywords for monitoring, and specific GitHub repositories and Reddit subreddits to watch.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • alembic/versions/0023_intelligence_events.py
    • Added a new Alembic migration script to create the intelligence_events table for storing community radar signals.
  • env.example
    • Updated with new environment variables for X (Twitter) bearer token, intelligence keywords, GitHub repositories, and Reddit subreddits.
  • src/paperbot/api/main.py
    • Imported and registered the new intelligence API router.
  • src/paperbot/api/routes/init.py
    • Added intelligence to the list of exported API routes.
  • src/paperbot/api/routes/intelligence.py
    • Added a new FastAPI route for the intelligence feed, including data models and logic for signal annotation and filtering.
  • src/paperbot/application/registries/default_sources.py
    • Registered new source descriptors for GitHub and HuggingFace, and updated the Twitter/X source to api_first acquisition mode with increased reliability.
  • src/paperbot/infrastructure/api_clients/init.py
    • Imported new GitHubRadarClient and XRecentSearchClient into the API clients module.
  • src/paperbot/infrastructure/api_clients/github_client.py
    • Added a new API client for interacting with the GitHub API to list recent issues.
  • src/paperbot/infrastructure/api_clients/x_client.py
    • Added a new API client for interacting with the X (Twitter) API to perform recent searches.
  • src/paperbot/infrastructure/services/intelligence_radar_service.py
    • Added a new service responsible for collecting, processing, and scoring intelligence signals from various external sources.
  • src/paperbot/infrastructure/stores/intelligence_store.py
    • Added a new data store for managing IntelligenceEventModel records, including methods for upserting and listing events.
  • src/paperbot/infrastructure/stores/models.py
    • Added the IntelligenceEventModel definition for the new intelligence_events database table.
  • tests/unit/test_intelligence_radar_service.py
    • Added new unit tests for the IntelligenceRadarService logic, covering signal matching and scoring.
  • tests/unit/test_intelligence_routes.py
    • Added new unit tests for the /intelligence/feed API endpoint, verifying response structure and service interaction.
  • tests/unit/test_intelligence_store.py
    • Added new unit tests for the IntelligenceStore, focusing on data serialization and datetime handling.
  • tests/unit/test_source_registry_modes.py
    • Updated an existing test to reflect the change in the Twitter/X source's acquisition mode.
Activity
  • The pull request author, jerry609, has implemented the intelligence radar backend route, persistence layer, migration, and service.
  • External source clients and source-registry support for community signal collection have been added.
  • New backend behavior is covered with route, service, store, and registry tests.
  • Validation steps include running pytest on specific unit test files: tests/unit/test_intelligence_radar_service.py, tests/unit/test_intelligence_routes.py, tests/unit/test_intelligence_store.py, and tests/unit/test_source_registry_modes.py.
  • The dashboard rendering for this feature is noted to be in a separate PR (feat: calm dashboard workspace #334), indicating a clear separation of concerns.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “intelligence/community radar” feature to PaperBot, including persistence, collection from external sources (Reddit/GitHub/HF/X), and a new API endpoint for serving a personalized intelligence feed.

Changes:

  • Introduces intelligence_events persistence (SQLAlchemy model + Alembic migration) and an IntelligenceStore for querying/upserting signals.
  • Adds IntelligenceRadarService to collect/score external signals (Reddit RSS + comments, GitHub Atom + Issues API, HF Daily Papers, X recent-search API).
  • Exposes /api/intelligence/feed with filtering/sorting/track matching plus unit test coverage for store/service/route behavior.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/unit/test_source_registry_modes.py Updates default source expectation for twitter_x acquisition mode.
tests/unit/test_intelligence_store.py Adds tests for datetime serialization and timezone restoration in IntelligenceStore.
tests/unit/test_intelligence_routes.py Adds route-level test for /api/intelligence/feed response shape and parameter forwarding.
tests/unit/test_intelligence_radar_service.py Adds unit tests for match logic, scoring, sorting, and refresh checks.
src/paperbot/infrastructure/stores/models.py Adds IntelligenceEventModel SQLAlchemy table mapping.
src/paperbot/infrastructure/stores/intelligence_store.py New store for upserting and listing intelligence events (JSON/list serialization helpers).
src/paperbot/infrastructure/services/intelligence_radar_service.py New service to collect, score, persist, and list intelligence feed signals.
src/paperbot/infrastructure/api_clients/x_client.py New X recent search API client wrapper.
src/paperbot/infrastructure/api_clients/github_client.py New GitHub issues API client wrapper.
src/paperbot/infrastructure/api_clients/init.py Exports new API clients.
src/paperbot/application/registries/default_sources.py Registers GitHub/HF sources and updates twitter_x descriptor to api_first.
src/paperbot/api/routes/intelligence.py New FastAPI route implementing /api/intelligence/feed with track matching and query building.
src/paperbot/api/routes/init.py Adds feed and intelligence route modules to exports/imports.
src/paperbot/api/main.py Includes the intelligence router in the FastAPI app.
env.example Adds env vars for X bearer token and intelligence tuning defaults.
alembic/versions/0023_intelligence_events.py New migration creating the intelligence_events table and index.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 6 to 10
reg = register_default_sources(SourceRegistry())
x = reg.get("twitter_x")
assert x is not None
assert x.acquisition_mode == AcquisitionMode.import_only
assert x.acquisition_mode == AcquisitionMode.api_first
assert x.enabled_by_default is False

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name still says the default Twitter/X source is "import_only", but the assertion now expects AcquisitionMode.api_first. Rename the test (or adjust the expectation) so the name matches the behavior being validated.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +14
"APIClient",
"GitHubRadarClient",
"SemanticScholarClient",
"XRecentSearchClient",

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file uses tab indentation inside the __all__ list. That’s inconsistent with the rest of the codebase (and will typically fight with Black/isort). Replace the tabs with spaces and let the formatter normalize the list.

Suggested change
"APIClient",
"GitHubRadarClient",
"SemanticScholarClient",
"XRecentSearchClient",
"APIClient",
"GitHubRadarClient",
"SemanticScholarClient",
"XRecentSearchClient",

Copilot uses AI. Check for mistakes.
Comment on lines +264 to +326
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_map = {
executor.submit(self._fetch_reddit_keyword_summary, keyword, allowed_subreddits): keyword
for keyword in profile.keywords[:4]
}
for future in as_completed(future_map):
try:
row = future.result()
except Exception:
continue
if row is None:
continue

keyword = row["keyword"]
entry_titles = [entry["title"] for entry in row["entries"]]
author_matches = _find_matches(profile.scholar_names, entry_titles)
repo_matches = _find_matches(profile.watch_repos, entry_titles)
external_id = f"reddit:keyword:{_slugify(keyword)}"
previous = self._store.get_event(user_id=user_id, external_id=external_id)
metric_value = len(row["entries"])
previous_value = int(previous.get("metric_value") or 0) if previous else 0
metric_delta = metric_value - previous_value
subreddits = row["subreddits"]
top_entry = row["entries"][0]
score, breakdown = _score_signal(
source="reddit",
kind="keyword_spike",
metric_value=metric_value,
metric_delta=metric_delta,
published_at=row["latest_published_at"],
keyword_hits=[keyword],
author_matches=author_matches,
repo_matches=repo_matches,
)
summary = (
f"24h mentions: {metric_value} across {', '.join(f'r/{name}' for name in subreddits[:3])}. "
f"Top post: {top_entry['title']}."
)
results.append(
{
"external_id": external_id,
"source": "reddit",
"source_label": "Reddit Search",
"kind": "keyword_spike",
"title": f"Reddit spike: {keyword}",
"summary": summary,
"url": top_entry["link"],
"keyword_hits": [keyword],
"author_matches": author_matches,
"repo_matches": repo_matches,
"metric_name": "mentions/24h",
"metric_value": metric_value,
"metric_delta": metric_delta,
"score": score,
"published_at": row["latest_published_at"],
"payload": {
"subreddits": subreddits,
"top_post": top_entry,
"entries": row["entries"][:5],
"score_breakdown": breakdown,
},
}
)

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ThreadPoolExecutor is used to issue concurrent HTTP requests, but all tasks share the same requests.Session instance (self._session). requests.Session is not guaranteed to be thread-safe, so this can lead to flaky networking behavior under concurrent refreshes. Consider using per-thread sessions (create a session inside each task), switching to plain requests.get per call, or guarding session usage with a lock.

Suggested change
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_map = {
executor.submit(self._fetch_reddit_keyword_summary, keyword, allowed_subreddits): keyword
for keyword in profile.keywords[:4]
}
for future in as_completed(future_map):
try:
row = future.result()
except Exception:
continue
if row is None:
continue
keyword = row["keyword"]
entry_titles = [entry["title"] for entry in row["entries"]]
author_matches = _find_matches(profile.scholar_names, entry_titles)
repo_matches = _find_matches(profile.watch_repos, entry_titles)
external_id = f"reddit:keyword:{_slugify(keyword)}"
previous = self._store.get_event(user_id=user_id, external_id=external_id)
metric_value = len(row["entries"])
previous_value = int(previous.get("metric_value") or 0) if previous else 0
metric_delta = metric_value - previous_value
subreddits = row["subreddits"]
top_entry = row["entries"][0]
score, breakdown = _score_signal(
source="reddit",
kind="keyword_spike",
metric_value=metric_value,
metric_delta=metric_delta,
published_at=row["latest_published_at"],
keyword_hits=[keyword],
author_matches=author_matches,
repo_matches=repo_matches,
)
summary = (
f"24h mentions: {metric_value} across {', '.join(f'r/{name}' for name in subreddits[:3])}. "
f"Top post: {top_entry['title']}."
)
results.append(
{
"external_id": external_id,
"source": "reddit",
"source_label": "Reddit Search",
"kind": "keyword_spike",
"title": f"Reddit spike: {keyword}",
"summary": summary,
"url": top_entry["link"],
"keyword_hits": [keyword],
"author_matches": author_matches,
"repo_matches": repo_matches,
"metric_name": "mentions/24h",
"metric_value": metric_value,
"metric_delta": metric_delta,
"score": score,
"published_at": row["latest_published_at"],
"payload": {
"subreddits": subreddits,
"top_post": top_entry,
"entries": row["entries"][:5],
"score_breakdown": breakdown,
},
}
)
for keyword in profile.keywords[:4]:
try:
row = self._fetch_reddit_keyword_summary(keyword, allowed_subreddits)
except Exception:
continue
if row is None:
continue
keyword = row["keyword"]
entry_titles = [entry["title"] for entry in row["entries"]]
author_matches = _find_matches(profile.scholar_names, entry_titles)
repo_matches = _find_matches(profile.watch_repos, entry_titles)
external_id = f"reddit:keyword:{_slugify(keyword)}"
previous = self._store.get_event(user_id=user_id, external_id=external_id)
metric_value = len(row["entries"])
previous_value = int(previous.get("metric_value") or 0) if previous else 0
metric_delta = metric_value - previous_value
subreddits = row["subreddits"]
top_entry = row["entries"][0]
score, breakdown = _score_signal(
source="reddit",
kind="keyword_spike",
metric_value=metric_value,
metric_delta=metric_delta,
published_at=row["latest_published_at"],
keyword_hits=[keyword],
author_matches=author_matches,
repo_matches=repo_matches,
)
summary = (
f"24h mentions: {metric_value} across {', '.join(f'r/{name}' for name in subreddits[:3])}. "
f"Top post: {top_entry['title']}."
)
results.append(
{
"external_id": external_id,
"source": "reddit",
"source_label": "Reddit Search",
"kind": "keyword_spike",
"title": f"Reddit spike: {keyword}",
"summary": summary,
"url": top_entry["link"],
"keyword_hits": [keyword],
"author_matches": author_matches,
"repo_matches": repo_matches,
"metric_name": "mentions/24h",
"metric_value": metric_value,
"metric_delta": metric_delta,
"score": score,
"published_at": row["latest_published_at"],
"payload": {
"subreddits": subreddits,
"top_post": top_entry,
"entries": row["entries"][:5],
"score_breakdown": breakdown,
},
}
)

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +155
with self._provider.session() as session:
row = session.execute(
select(IntelligenceEventModel).where(
IntelligenceEventModel.user_id == user_id,
IntelligenceEventModel.external_id == external_id,
)
).scalar_one_or_none()
if row is None:
row = IntelligenceEventModel(
user_id=user_id,
external_id=external_id,
created_at=now,
)

row.source = (source or "unknown").strip()[:32]
row.source_label = (source_label or row.source).strip()[:64]
row.kind = (kind or "signal").strip()[:64]
row.title = (title or "Untitled signal").strip()
row.summary = (summary or "").strip()
row.url = (url or "").strip()
row.repo_full_name = (repo_full_name or "").strip()[:128]
row.author_name = (author_name or "").strip()[:128]
row.keyword_hits_json = _dump_list(keyword_hits)
row.author_matches_json = _dump_list(author_matches)
row.repo_matches_json = _dump_list(repo_matches)
row.metric_name = (metric_name or "").strip()[:64]
row.metric_value = int(metric_value or 0)
row.metric_delta = int(metric_delta or 0)
row.score = float(score or 0.0)
row.published_at = _parse_datetime(published_at) or row.published_at or now
row.detected_at = now
row.updated_at = now
row.payload_json = _dump_dict(payload)

session.add(row)
session.commit()
session.refresh(row)
return self._row_to_dict(row)

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upsert_event() does a read-then-insert/update without handling the race where two refreshes insert the same (user_id, external_id) concurrently. With the UNIQUE constraint, this can raise an IntegrityError and fail the refresh/background task. Consider using a database-level upsert (e.g., INSERT .. ON CONFLICT DO UPDATE for SQLite/Postgres) or catching IntegrityError on commit() and re-selecting/updating the existing row.

Copilot uses AI. Check for mistakes.
op.create_table(
"intelligence_events",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("user_id", sa.String(64), nullable=False, index=True),

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ORM model sets user_id default to "default", but the migration creates user_id as NOT NULL without a server_default. Any non-ORM inserts (or future backfills) that omit user_id will fail at the DB layer. Consider adding server_default="default" (or an explicit sa.text(...)) to keep the DB schema aligned with the model default.

Suggested change
sa.Column("user_id", sa.String(64), nullable=False, index=True),
sa.Column("user_id", sa.String(64), nullable=False, server_default="default", index=True),

Copilot uses AI. Check for mistakes.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new 'intelligence_events' table and API endpoint to support a community radar/intelligence feed feature. It adds new API clients for GitHub and X (Twitter) and integrates HuggingFace daily papers, Reddit, GitHub activity, and X search results into an intelligence radar service. The service collects and scores events based on keywords, authors, and repositories, storing them in the new database table. Environment variables for X bearer tokens and intelligence feed configuration have also been added. Review comments highlight a high-severity Insecure Direct Object Reference (IDOR) vulnerability in the user_id parameter, suggesting proper authentication and authorization. Other comments point out several medium-severity issues, including inefficient database queries in the intelligence feed, the use of string literals for integer and float server_default values in the database schema, and numerous instances of catching generic Exception types which should be replaced with more specific exception handling and logging for better debugging and robustness. Additionally, there is a code duplication issue with _utcnow and _parse_datetime utility functions that should be refactored into a shared module.

@router.get("/intelligence/feed", response_model=IntelligenceFeedResponse)
def get_intelligence_feed(
background_tasks: BackgroundTasks,
user_id: str = Query("default"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The user_id parameter is taken directly from the query string without any authentication or authorization check. In a multi-user environment, this allows any user to access the intelligence feed, research tracks, and keywords of any other user by simply providing their user_id in the request. This is a classic Insecure Direct Object Reference (IDOR) vulnerability.

To remediate this, you should:

  1. Implement an authentication mechanism (e.g., JWT, session cookies).
  2. Retrieve the user_id from the authenticated user's session or token instead of a query parameter.
  3. If the user_id must be provided in the query, verify that the authenticated user has permission to access the data for that user_id.

sa.Column("metric_name", sa.String(64), nullable=False, server_default=""),
sa.Column("metric_value", sa.Integer(), nullable=False, server_default="0"),
sa.Column("metric_delta", sa.Integer(), nullable=False, server_default="0"),
sa.Column("score", sa.Float(), nullable=False, server_default="0.0", index=True),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For sa.Float() columns, it's generally better practice to use a float literal for server_default (e.g., 0.0) instead of a string literal ("0.0"). While most databases might implicitly convert this, being explicit avoids potential type mismatches or unexpected behavior in different database systems.

Suggested change
sa.Column("score", sa.Float(), nullable=False, server_default="0.0", index=True),
sa.Column("score", sa.Float(), nullable=False, server_default=0.0, index=True),

Comment on lines +44 to +45
sa.Column("metric_value", sa.Integer(), nullable=False, server_default="0"),
sa.Column("metric_delta", sa.Integer(), nullable=False, server_default="0"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the score column, for sa.Integer() columns, it's best to use an integer literal for server_default (e.g., 0) instead of a string literal ("0").

Suggested change
sa.Column("metric_value", sa.Integer(), nullable=False, server_default="0"),
sa.Column("metric_delta", sa.Integer(), nullable=False, server_default="0"),
sa.Column("metric_value", sa.Integer(), nullable=False, server_default=0),
sa.Column("metric_delta", sa.Integer(), nullable=False, server_default=0),

Comment on lines +128 to +131
rows = service.list_feed(
user_id=user_id,
limit=max(int(limit) * 10, 50),
source=source,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The list_feed call in the service fetches max(int(limit) * 10, 50) items, which means it will always fetch at least 50 items from the database, even if the requested limit from the API is much smaller (e.g., limit=6). This can lead to inefficient database queries and unnecessary data transfer if the filtering applied afterwards significantly reduces the result set. Consider pushing more of the filtering and limiting logic down to the IntelligenceStore.list_events method to optimize performance, or adjust the candidate_limit to be a smaller multiple of limit.

Comment on lines +197 to +198
except Exception:
tracks = []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a generic Exception can hide important details about what went wrong. It's better to catch more specific exceptions (e.g., sqlalchemy.exc.SQLAlchemyError for database issues) and/or log the full exception traceback to aid debugging. This applies to other similar try...except Exception blocks in the codebase.

Comment on lines +428 to +429
except Exception:
grouped.setdefault(repo, {})[feed_type] = []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a generic Exception can hide important details about what went wrong during the GitHub feed fetching. It's better to catch more specific exceptions and/or log the full exception traceback to aid debugging.

Comment on lines +533 to +534
issues = self._github_client.list_recent_issues(repo, per_page=20)
except Exception:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a generic Exception can hide important details about what went wrong during the list_recent_issues call. It's better to catch more specific exceptions and/or log the full exception traceback to aid debugging.

Comment on lines +604 to +605
records = future.result()
except Exception:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a generic Exception can hide important details about what went wrong during the _hf_daily.search call. It's better to catch more specific exceptions and/or log the full exception traceback to aid debugging.

Comment on lines +689 to +690
except Exception:
tweets = []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a generic Exception can hide important details about what went wrong during the _x_client.search_recent call. It's better to catch more specific exceptions and/or log the full exception traceback to aid debugging.

Comment on lines +13 to +32
def _utcnow() -> datetime:
return datetime.now(timezone.utc)


def _parse_datetime(value: Any) -> Optional[datetime]:
if isinstance(value, datetime):
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
if not value:
return None

text = str(value).strip()
if not text:
return None
if text.endswith("Z"):
text = f"{text[:-1]}+00:00"
try:
parsed = datetime.fromisoformat(text)
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
except Exception:
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The utility functions _utcnow and _parse_datetime are duplicated in src/paperbot/infrastructure/services/intelligence_radar_service.py. To improve maintainability and avoid code duplication, these common utility functions should be moved to a shared module (e.g., paperbot.utils.datetime_utils) and imported where needed.

@jerry609
jerry609 merged commit 366e021 into dev Mar 11, 2026
4 checks passed
@jerry609
jerry609 deleted the pr/intelligence-radar-backend branch March 12, 2026 03:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants