mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* feat: bot commands & async dispatch — /ask, /chat, /history, /strategies, NL routing (#648) * feat: bot commands & async dispatch — /ask, /chat, /history, /strategies, NL routing - CommandDispatcher with dispatch_async() and NL routing - /ask multi-stock analysis (parallel execution, portfolio overlay) - /chat free-form conversation with session persistence - /history per-user session isolation (colon delimiter format) - /strategies with category grouping and activation status - Two-layer NL routing: regex pre-filter → LLM intent parsing - Async webhook handler for FastAPI integration - Feishu stream: capped ThreadPoolExecutor replaces unbounded threads - DingTalk stream: direct async handler awaiting * fix: address PR #648 review comments - ask.py: simplify validate_args error message, add _merge_code_args() for comma+space separated codes - chat.py: simplify validate_args error message - feishu_stream.py: shutdown ThreadPoolExecutor in stop() - test_ask_command.py, test_bot_dispatcher_async.py: replace sys.modules litellm hack with litellm_stub * fix(bot): preserve ask strategy args and serialize Feishu replies * fix(strategies): remove activate() side-effect from /strategies command /strategies is a read-only listing command but was calling sm.activate() which mutates the skill manager state. Replace with a config-based set lookup (configured_active) to derive ✅/⬜ status without any write operations. This makes the command purely read-only and eliminates the risk of altering subsequent /ask or /chat agent skill sets. (cherry picked from commita472b824a8) * feat: deep research agent & event monitor — /research command, alert rules, scheduler integration (#649) * feat: deep research agent & event monitor — /research command, alert rules, scheduler integration - ResearchAgent with 3-phase approach (decompose → research → synthesise) - Token budget tracking for deep research queries - /research bot command with aliases (/深研, /deepsearch) - REST endpoint POST /api/v1/agent/research - EventMonitor with PriceAlert/VolumeAlert rules - Scheduler background task system for periodic alert polling - EventMonitor config validation in system_config_service - main.py: EventMonitor wiring into schedule mode - Unsupported rule type rejection at config and runtime level * fix: address PR #649 review comments - events.py: fix to_dict_list docstring, validate created_at as float, raise ValueError on non-dict rules - research.py: remove unused import uuid - agent.py: replace nested ThreadPoolExecutor with asyncio.wait_for, use Field(default_factory=list), pass stock_code context - scheduler.py: clamp minimum interval_seconds to 30s - test_multi_agent.py: reduce sleep 3s→0.05s and timeout 1s→0.01s to speed up tests * fix: normalize price alert direction and tighten research ticker regex - events.py from_dict_list: .lower() on deserialized direction so _check_price matches correctly (fixes silent missed alerts for mixed-case config like {"direction":"Above"}) - research.py: restrict US ticker auto-detection to 1-4 chars (was 1-5); avoids misclassifying common words (MACRO, TREND) as stock codes in /research topic queries (cherry picked from commit28126db5bd) * fix: reconcile recovered agent features with current main * fix: address review feedback and backend gate failures * fix: address remaining review feedback * fix: restore bot agent gating and research timeout handling
99 lines
2.9 KiB
Python
99 lines
2.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Unit tests for Feishu Stream message ordering guarantees."""
|
|
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from datetime import datetime
|
|
|
|
from bot.models import BotMessage, BotResponse, ChatType
|
|
from bot.platforms.feishu_stream import FeishuStreamHandler
|
|
|
|
|
|
class _DummyReplyClient:
|
|
def __init__(self):
|
|
self.calls = []
|
|
self._lock = threading.Lock()
|
|
|
|
def reply_text(self, message_id, text, at_user=False, user_id=None):
|
|
with self._lock:
|
|
self.calls.append(
|
|
{
|
|
"message_id": message_id,
|
|
"text": text,
|
|
"at_user": at_user,
|
|
"user_id": user_id,
|
|
}
|
|
)
|
|
return True
|
|
|
|
|
|
def _make_message(
|
|
message_id: str,
|
|
*,
|
|
user_id: str = "u1",
|
|
chat_id: str = "c1",
|
|
chat_type: ChatType = ChatType.PRIVATE,
|
|
) -> BotMessage:
|
|
return BotMessage(
|
|
platform="feishu",
|
|
message_id=message_id,
|
|
user_id=user_id,
|
|
user_name=user_id,
|
|
chat_id=chat_id,
|
|
chat_type=chat_type,
|
|
content="/chat hello",
|
|
raw_content="/chat hello",
|
|
mentioned=True,
|
|
timestamp=datetime.now(),
|
|
)
|
|
|
|
|
|
class FeishuStreamOrderingTestCase(unittest.TestCase):
|
|
def test_same_conversation_is_processed_fifo(self):
|
|
reply_client = _DummyReplyClient()
|
|
processed = []
|
|
|
|
def on_message(message: BotMessage) -> BotResponse:
|
|
if message.message_id == "m1":
|
|
time.sleep(0.05)
|
|
processed.append(message.message_id)
|
|
return BotResponse.text_response(message.message_id)
|
|
|
|
handler = FeishuStreamHandler(on_message, reply_client)
|
|
try:
|
|
handler._enqueue_message(_make_message("m1"))
|
|
handler._enqueue_message(_make_message("m2"))
|
|
|
|
deadline = time.time() + 1.0
|
|
while len(reply_client.calls) < 2 and time.time() < deadline:
|
|
time.sleep(0.01)
|
|
|
|
self.assertEqual(processed, ["m1", "m2"])
|
|
self.assertEqual(
|
|
[call["message_id"] for call in reply_client.calls],
|
|
["m1", "m2"],
|
|
)
|
|
finally:
|
|
handler.shutdown(wait=True)
|
|
|
|
def test_group_chat_uses_user_scoped_ordering_key(self):
|
|
handler = FeishuStreamHandler(lambda _message: BotResponse.text_response("ok"), _DummyReplyClient())
|
|
try:
|
|
key_a = handler._conversation_key(
|
|
_make_message("m1", user_id="u1", chat_id="group-1", chat_type=ChatType.GROUP)
|
|
)
|
|
key_b = handler._conversation_key(
|
|
_make_message("m2", user_id="u2", chat_id="group-1", chat_type=ChatType.GROUP)
|
|
)
|
|
|
|
self.assertEqual(key_a, "group-1:u1")
|
|
self.assertEqual(key_b, "group-1:u2")
|
|
self.assertNotEqual(key_a, key_b)
|
|
finally:
|
|
handler.shutdown(wait=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|