Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ class Config:
title = "hot_topic_detail"


def parse_hot_topics_response(text):
if not text or not text.strip():
raise ValueError("AI response is empty")

parsed = json.loads(repair_json(text))
if not isinstance(parsed, dict):
raise ValueError("AI response is invalid: expected object with hot_topics")

topics = parsed.get("hot_topics")
if not isinstance(topics, list):
raise ValueError("AI response is invalid: hot_topics must be a list")

for index, topic in enumerate(topics):
if not isinstance(topic, dict):
raise ValueError(f"AI response is invalid: hot_topics[{index}] must be an object")

return topics
Comment on lines +63 to +79

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

To further harden the AI response parsing and prevent unhandled exceptions:

  1. Handle JSON decoding errors gracefully: If repair_json(text) produces invalid JSON, json.loads will raise a json.JSONDecodeError. Wrapping this in a try-except block and raising a clear ValueError keeps the error messages consistent and informative.
  2. Prevent potential KeyError: In getTodayTopNews(), needKnow['hot_label'] is accessed directly (line 408). If the AI response contains a topic object that is missing the hot_label key, this will raise a KeyError. Validating that hot_label is present in each topic dictionary prevents this cascading failure.
def parse_hot_topics_response(text):
    if not text or not text.strip():
        raise ValueError("AI response is empty")

    try:
        parsed = json.loads(repair_json(text))
    except json.JSONDecodeError as e:
        raise ValueError(f"AI response is not valid JSON: {e}")

    if not isinstance(parsed, dict):
        raise ValueError("AI response is invalid: expected object with hot_topics")

    topics = parsed.get("hot_topics")
    if not isinstance(topics, list):
        raise ValueError("AI response is invalid: hot_topics must be a list")

    for index, topic in enumerate(topics):
        if not isinstance(topic, dict):
            raise ValueError(f"AI response is invalid: hot_topics[{index}] must be an object")
        if "hot_label" not in topic:
            raise ValueError(f"AI response is invalid: hot_topics[{index}] is missing 'hot_label'")

    return topics



limiter = Limiter(key_func=get_remote_address, default_limits=["30 per minute"], storage_uri=f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}")
backoff = ExponentialBackoff(cap=2, base=2)
retry = Retry(backoff=backoff, retries=10)
Expand Down Expand Up @@ -379,8 +398,8 @@ async def getTodayTopNews():
"user": "请从下方数据中选出5条你认为最应该让我知道的内容,返回json格式数据,不要改变原有的数据内容,返回格式{'hot_topics': [{hot_label:'',hot_url:'',hot_value:''}]}\ndata:" + json.dumps(true_sites_data)
}
, HotTopics.model_json_schema())
today_top_news_data = json.loads(repair_json(text))
need_knows = await parse_detail(today_top_news_data.get("hot_topics", []))
hot_topics = parse_hot_topics_response(text)
need_knows = await parse_detail(hot_topics)
summarizes = []
for needKnow in need_knows:
err = 3
Expand Down
80 changes: 80 additions & 0 deletions tests/test_today_top_news.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import unittest
import sys
import types
from unittest.mock import patch

config = types.ModuleType("config")
config.REDIS_PASSWORD = ""
config.REDIS_HOST = "localhost"
config.REDIS_PORT = 6379
config.REDIS_DB = 0
config.PG_HOST = "localhost"
config.PG_PORT = 5432
config.PG_USER = "user"
config.PG_PASSWORD = "password"
config.PG_DB = "db"
config.api_url = "https://example.com"
config.api_headers = {}
config.news_sites = ["澎湃新闻"]
config.EMAIL = {
"user": "test@example.com",
"host": "smtp.example.com",
"port": 587,
"password": "password",
}
sys.modules["config"] = config

import app


class FakeRedis:
async def get(self, key):
return None

async def set(self, *args, **kwargs):
return True

async def delete(self, *args, **kwargs):
return 1

async def setex(self, *args, **kwargs):
return True


class TodayTopNewsTest(unittest.IsolatedAsyncioTestCase):
async def test_empty_model_response_returns_clear_error(self):
async def fake_get_data(item_id):
return {
"code": 200,
"msg": "success",
"data": [
{
"name": "澎湃新闻",
"data": [
{
"hot_label": "A",
"hot_url": "https://example.com",
"hot_value": "1",
}
],
}
],
}

async def fake_chat_with_model(messages, response_format):
return ""

with patch.object(app, "redis_client", FakeRedis()), patch.object(
app, "get_data", fake_get_data
), patch.object(app, "chatWithModel", fake_chat_with_model), patch.object(
app.logging, "error"
):
result = await app.getTodayTopNews()

self.assertEqual(result["code"], 500)
self.assertIn("AI response is empty", result["msg"])
self.assertNotIn("object has no attribute", result["msg"])


if __name__ == "__main__":
unittest.main()