diff --git a/backend/app/services/simulation_manager.py b/backend/app/services/simulation_manager.py index 8d6b78c3..5cd07bf4 100644 --- a/backend/app/services/simulation_manager.py +++ b/backend/app/services/simulation_manager.py @@ -334,6 +334,16 @@ class SimulationManager: elif state.enable_twitter: realtime_output_path = os.path.join(sim_dir, "twitter_profiles.csv") realtime_platform = "twitter" + + # Nền tảng dùng cho metadata cost. + if state.enable_twitter and state.enable_reddit: + runtime_platform = "parallel" + elif state.enable_twitter: + runtime_platform = "twitter" + elif state.enable_reddit: + runtime_platform = "reddit" + else: + runtime_platform = None profiles = generator.generate_profiles_from_entities( entities=filtered.entities, @@ -343,6 +353,7 @@ class SimulationManager: parallel_count=parallel_profile_count, # Số dòng luồng Async realtime_output_path=realtime_output_path, # Lưu log thời gian thực output_platform=realtime_platform, # Đuôi file xuất + metadata_platform=runtime_platform, simulation_id=simulation_id, project_id=state.project_id, ) diff --git a/backend/app/services/text_processor.py b/backend/app/services/text_processor.py index 5e9d0c28..625793b9 100644 --- a/backend/app/services/text_processor.py +++ b/backend/app/services/text_processor.py @@ -54,7 +54,7 @@ class TextProcessor: # Xoá các dòng trống liên tiếp (Chỉ giữ lại tối đa 2 lần xuống dòng liên tiếp) text = re.sub(r'\n{3,}', '\n\n', text) - # 移除行首行尾空白 + # Loại bỏ khoảng trắng đầu và cuối lines = [line.strip() for line in text.split('\n')] text = '\n'.join(lines) diff --git a/backend/scripts/llm_cost_patch.py b/backend/scripts/llm_cost_patch.py index b02cde31..fe6d16fa 100644 --- a/backend/scripts/llm_cost_patch.py +++ b/backend/scripts/llm_cost_patch.py @@ -20,6 +20,36 @@ def _build_metadata(model: str) -> Dict[str, Any]: return metadata +def _normalize_messages_for_qwen(messages: Any) -> Any: + """Ensure Qwen-compatible ordering: a single system message at index 0.""" + if not isinstance(messages, list): + return messages + + if not messages: + return messages + + system_contents = [] + non_system_messages = [] + + for item in messages: + if isinstance(item, dict) and str(item.get("role", "")).lower() == "system": + content = item.get("content") + if content is not None and str(content).strip(): + system_contents.append(str(content)) + continue + non_system_messages.append(item) + + # No system messages detected, keep original payload. + if not system_contents: + return messages + + merged_system = { + "role": "system", + "content": "\n\n".join(system_contents), + } + return [merged_system] + non_system_messages + + def install_openai_cost_patch( *, simulation_id: Optional[str], @@ -58,6 +88,9 @@ def install_openai_cost_patch( def sync_create_wrapper(self, *args, **kwargs): model = kwargs.get("model", "unknown_model") + if "qwen" in str(model).lower() and "messages" in kwargs: + kwargs = dict(kwargs) + kwargs["messages"] = _normalize_messages_for_qwen(kwargs.get("messages")) response = original_sync_create(self, *args, **kwargs) try: record_llm_cost( @@ -71,6 +104,9 @@ def install_openai_cost_patch( async def async_create_wrapper(self, *args, **kwargs): model = kwargs.get("model", "unknown_model") + if "qwen" in str(model).lower() and "messages" in kwargs: + kwargs = dict(kwargs) + kwargs["messages"] = _normalize_messages_for_qwen(kwargs.get("messages")) response = await original_async_create(self, *args, **kwargs) try: record_llm_cost( diff --git a/backend/scripts/run_parallel_simulation.py b/backend/scripts/run_parallel_simulation.py index 689fc103..fb9eda66 100644 --- a/backend/scripts/run_parallel_simulation.py +++ b/backend/scripts/run_parallel_simulation.py @@ -361,7 +361,7 @@ class ParallelIPCHandler: """ # Nếu có chỉ định nền tảng, chỉ phỏng vấn trên nền tảng đó if platform in ("twitter", "reddit"): - result = await self._interview_single_platform(agent_id, prompt, platform) + result = await self._interview_single_platform(agent_id, prompt, platform) if "error" in result: self.send_response(command_id, "failed", error=result["error"]) @@ -996,11 +996,27 @@ def create_model(config: Dict[str, Any], use_boost: bool = False): config: Dictionary cấu hình mô phỏng use_boost: Có dùng cấu hình LLM tăng tốc hay không (nếu khả dụng) """ - # Kiểm tra có cấu hình tăng tốc không + def _is_placeholder(value: str) -> bool: + v = (value or "").strip().lower() + return v in {"your_api_key_here", "your_base_url_here", "your_model_name_here"} + + def _is_http_url(value: str) -> bool: + v = (value or "").strip().lower() + return v.startswith("http://") or v.startswith("https://") + + # Kiểm tra có cấu hình tăng tốc hợp lệ không boost_api_key = os.environ.get("LLM_BOOST_API_KEY", "") boost_base_url = os.environ.get("LLM_BOOST_BASE_URL", "") boost_model = os.environ.get("LLM_BOOST_MODEL_NAME", "") - has_boost_config = bool(boost_api_key) + has_boost_config = ( + bool(boost_api_key.strip()) + and bool(boost_model.strip()) + and bool(boost_base_url.strip()) + and not _is_placeholder(boost_api_key) + and not _is_placeholder(boost_base_url) + and not _is_placeholder(boost_model) + and _is_http_url(boost_base_url) + ) # Chọn LLM theo tham số và trạng thái cấu hình if use_boost and has_boost_config: @@ -1011,6 +1027,8 @@ def create_model(config: Dict[str, Any], use_boost: bool = False): config_label = "[Boost LLM]" else: # Dùng cấu hình chung + if use_boost and not has_boost_config: + print("[Boost LLM] Invalid or placeholder boost config, fallback to [General LLM].") llm_api_key = os.environ.get("LLM_API_KEY", "") llm_base_url = os.environ.get("LLM_BASE_URL", "") llm_model = os.environ.get("LLM_MODEL_NAME", "") @@ -1028,6 +1046,10 @@ def create_model(config: Dict[str, Any], use_boost: bool = False): raise ValueError("Missing API key config. Please set LLM_API_KEY in the project root .env file") if llm_base_url: + if not _is_http_url(llm_base_url): + raise ValueError( + f"Invalid LLM base URL: {llm_base_url}. It must start with http:// or https://" + ) os.environ["OPENAI_API_BASE_URL"] = llm_base_url print(f"{config_label} model={llm_model}, base_url={llm_base_url[:40] if llm_base_url else 'default'}...") diff --git a/frontend/src/components/Step4Report.vue b/frontend/src/components/Step4Report.vue index ad7b5e33..db704ce3 100644 --- a/frontend/src/components/Step4Report.vue +++ b/frontend/src/components/Step4Report.vue @@ -862,7 +862,7 @@ const parseInterview = (text) => { const quotesText = quotesMatch[1] // Prioritize matching > "text" format let quoteMatches = quotesText.match(/> "([^"]+)"/g) - // Backtrack: Matches > "text" or > \u201Ctext\u201D (Chinese quotation marks) + // Backtrack: Matches > "text" or > \u201Ctext\u201D (Vietnamese quotation marks) if (!quoteMatches) { quoteMatches = quotesText.match(/> [\u201C""]([^\u201D""]+)[\u201D""]/g) } @@ -2089,9 +2089,9 @@ const extractFinalContent = (response) => { } // Try to find the content after "Final Answer:" - const chineseFinalMatch = response.match(/Final Answer[::]\s*\n*([\s\S]*)$/i) - if (chineseFinalMatch) { - return chineseFinalMatch[1].trim() + const vietnameseFinalMatch = response.match(/Final Answer[::]\s*\n*([\s\S]*)$/i) + if (vietnameseFinalMatch) { + return vietnameseFinalMatch[1].trim() } // Nếu bắt đầu bằng ## hoặc # hoặc >, có thể là nội dung markdown trực tiếp diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue index 5a8728d8..73545ae5 100644 --- a/frontend/src/views/Home.vue +++ b/frontend/src/views/Home.vue @@ -176,7 +176,7 @@ diff --git a/package.json b/package.json index 63ace21a..a23ca5b9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mirofish", "version": "0.1.0", - "description": "MiroFish - 简洁通用的群体智能引擎,预测万物", + "description": "MiroFish - A simple and versatile swarm intelligence engine that predicts everything", "scripts": { "setup": "npm install && cd frontend && npm install", "setup:backend": "cd backend && uv sync",